diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 000000000..e0d3b0831 --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,6 @@ +Jonathan Shimwell +John Billingsley +Remi Delaporte-Mathurin +Declan Morbey +Matthew Bluteau +Patrick Shriwise \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..af81ae422 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,213 @@ +# This dockerfile can be built in a few different ways. +# Docker build commands must be run from within the base repository directory +# +# There are build args availalbe for specifying the: +# - cq_version +# The version of CadQuery to use master or 2. +# Default is 2. +# Options: [master, 2] +# +# - include_neutronics +# If software dependencies needed for neutronics simulations should be +# included true or false. +# Default is false. +# Options: [true, false] +# +# - compile_cores +# The number of CPU cores to compile the image with. +# Default is 1. +# Options: [1, 2, 3, 4, 5, 6...] +# +# Example builds: +# Building using the defaults (cq_version 2, no neutronics and 1 core compile) +# docker build -t ukaea/paramak . +# +# Building to include cadquery master, neutronics dependencies and use 8 cores. +# Run command from within the base repository directory +# docker build -t ukaea/paramak --build-arg include_neutronics=true --build-arg compile_cores=8 --build-arg cq_version=master . + +# Once build the dockerimage can be run in a few different ways. +# +# Run with the following command for a terminal notebook interface +# docker run -it ukaea/paramak . +# +# Run with the following command for a jupyter notebook interface +# docker run -p 8888:8888 ukaea/paramak /bin/bash -c "jupyter notebook --notebook-dir=/examples --ip='*' --port=8888 --no-browser --allow-root" + + +# Once built, the docker image can be tested with either of the following commands +# docker run --rm ukaea/paramak pytest /tests +# docker run --rm ukaea/paramak /bin/bash -c "cd .. && bash run_tests.sh" + +FROM continuumio/miniconda3 + +# By default this Dockerfile builds with the latest release of CadQuery 2 +ARG cq_version=2 +ARG include_neutronics=false +ARG compile_cores=1 + +ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 \ + PATH=/opt/openmc/bin:/opt/NJOY2016/build:$PATH \ + LD_LIBRARY_PATH=/opt/openmc/lib:$LD_LIBRARY_PATH \ + CC=/usr/bin/mpicc CXX=/usr/bin/mpicxx \ + DEBIAN_FRONTEND=noninteractive + +RUN apt-get update -y && \ + apt-get upgrade -y + +RUN apt-get install -y libgl1-mesa-glx libgl1-mesa-dev libglu1-mesa-dev \ + freeglut3-dev libosmesa6 libosmesa6-dev \ + libgles2-mesa-dev && \ + apt-get clean + +# Installing CadQuery +# jupyter is installed before cadquery to avoid a conflict +RUN echo installing CadQuery version $cq_version && \ + conda install jupyter -y --quiet && \ + conda install -c cadquery -c conda-forge cadquery="$cq_version" && \ + conda clean -afy + +# Install neutronics dependencies from Debian package manager +RUN if [ "$include_neutronics" = "true" ] ; \ + then echo installing with include_neutronics=true ; \ + apt-get install -y \ + wget git gfortran g++ cmake \ + mpich libmpich-dev libhdf5-serial-dev libhdf5-mpich-dev \ + imagemagick ; \ + fi + +# install addition packages required for MOAB +RUN if [ "$include_neutronics" = "true" ] ; \ + then echo installing with include_neutronics=true ; \ + apt-get --yes install libeigen3-dev ; \ + apt-get --yes install libblas-dev ; \ + apt-get --yes install liblapack-dev ; \ + apt-get --yes install libnetcdf-dev ; \ + apt-get --yes install libtbb-dev ; \ + apt-get --yes install libglfw3-dev ; \ + fi + +# Clone and install NJOY2016 +RUN if [ "$include_neutronics" = "true" ] ; \ + then git clone https://github.com/njoy/NJOY2016 /opt/NJOY2016 ; \ + cd /opt/NJOY2016 ; \ + mkdir build ; \ + cd build ; \ + cmake -Dstatic=on .. ; \ + make 2>/dev/null ; \ + make install ; \ + fi + +# Clone and install Embree +RUN if [ "$include_neutronics" = "true" ] ; \ + then git clone https://github.com/embree/embree ; \ + cd embree ; \ + mkdir build ; \ + cd build ; \ + cmake .. -DCMAKE_INSTALL_PREFIX=.. \ + -DEMBREE_ISPC_SUPPORT=OFF ; \ + make -j"$compile_cores" ; \ + make -j"$compile_cores" install ; \ + fi + +# Clone and install MOAB +RUN if [ "$include_neutronics" = "true" ] ; \ + then pip install --upgrade numpy cython ; \ + mkdir MOAB ; \ + cd MOAB ; \ + mkdir build ; \ + git clone --single-branch --branch develop https://bitbucket.org/fathomteam/moab/ ; \ + cd build ; \ + cmake ../moab -DENABLE_HDF5=ON \ + -DENABLE_NETCDF=ON \ + -DBUILD_SHARED_LIBS=OFF \ + -DENABLE_FORTRAN=OFF \ + -DCMAKE_INSTALL_PREFIX=/MOAB ; \ + make -j"$compile_cores" ; \ + make -j"$compile_cores" install ; \ + rm -rf * ; \ + cmake ../moab -DBUILD_SHARED_LIBS=ON \ + -DENABLE_HDF5=ON \ + -DENABLE_PYMOAB=ON \ + -DENABLE_BLASLAPACK=OFF \ + -DENABLE_FORTRAN=OFF \ + -DCMAKE_INSTALL_PREFIX=/MOAB ; \ + make -j"$compile_cores" ; \ + make -j"$compile_cores" install ; \ + cd pymoab ; \ + bash install.sh ; \ + python setup.py install ; \ + fi + + +# Clone and install Double-Down +RUN if [ "$include_neutronics" = "true" ] ; \ + then git clone https://github.com/pshriwise/double-down ; \ + cd double-down ; \ + mkdir build ; \ + cd build ; \ + cmake .. -DCMAKE_INSTALL_PREFIX=.. \ + -DMOAB_DIR=/MOAB \ + -DEMBREE_DIR=/embree/lib/cmake/embree-3.12.1 \ + -DEMBREE_ROOT=/embree/lib/cmake/embree-3.12.1 ; \ + make -j"$compile_cores" ; \ + make -j"$compile_cores" install ; \ + fi + +# Clone and install DAGMC +RUN if [ "$include_neutronics" = "true" ] ; \ + then mkdir DAGMC ; \ + cd DAGMC ; \ + git clone -b develop https://github.com/svalinn/dagmc ; \ + mkdir build ; \ + cd build ; \ + cmake ../dagmc -DBUILD_TALLY=ON \ + -DCMAKE_INSTALL_PREFIX=/dagmc/ \ + -DMOAB_DIR=/MOAB \ + -DBUILD_STATIC_LIBS=OFF \ + -DBUILD_STATIC_EXE=OFF ; \ + make -j"$compile_cores" install ; \ + rm -rf /DAGMC/dagmc /DAGMC/build ; \ + fi + +# Clone and install OpenMC with DAGMC +RUN if [ "$include_neutronics" = "true" ] ; \ + then git clone --recurse-submodules https://github.com/openmc-dev/openmc.git /opt/openmc ; \ + cd /opt/openmc ; \ + mkdir build ; \ + cd build ; \ + cmake -Doptimize=on -Ddagmc=ON \ + -DDAGMC_DIR=/DAGMC/ \ + -DHDF5_PREFER_PARALLEL=on .. ; \ + make -j"$compile_cores" ; \ + make -j"$compile_cores" install ; \ + cd .. ; \ + pip install -e .[test] ; \ + /opt/openmc/tools/ci/download-xs.sh ; \ + fi + +ENV OPENMC_CROSS_SECTIONS=/root/nndc_hdf5/cross_sections.xml + +# Copies over the Paramak code from the local repository + +RUN if [ "$include_neutronics" = "true" ] ; \ + then pip install neutronics_material_maker ; \ + pip install parametric_plasma_source ; \ + fi + +COPY requirements.txt requirements.txt +RUN pip install -r requirements.txt + + +# Copy over the source code, examples and tests +COPY run_tests.sh run_tests.sh +COPY paramak paramak/ +COPY examples examples/ +COPY setup.py setup.py +COPY tests tests/ +COPY README.md README.md + +# using setup.py instead of pip due to https://github.com/pypa/pip/issues/5816 +RUN python setup.py install + +WORKDIR examples diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..c0b0c890a --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 UKAEA + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..78d0ae4fb --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include README.md +include requirements.txt +include LICENSE.txt \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..ab72dd4db --- /dev/null +++ b/README.md @@ -0,0 +1,284 @@ + +[![N|Python](https://www.python.org/static/community_logos/python-powered-w-100x40.png)](https://www.python.org) +[![CircleCI](https://circleci.com/gh/ukaea/paramak/tree/main.svg?style=svg)](https://circleci.com/gh/ukaea/paramak/tree/main) +[![codecov](https://codecov.io/gh/ukaea/paramak/branch/main/graph/badge.svg)](https://codecov.io/gh/ukaea/paramak) +[![PyPI version](https://badge.fury.io/py/paramak.svg)](https://badge.fury.io/py/paramak) +[![Documentation Status](https://readthedocs.org/projects/paramak/badge/?version=main)](https://paramak.readthedocs.io/en/main/?badge=main) +[![Docker Cloud Build Status](https://img.shields.io/docker/cloud/build/ukaea/paramak)](https://hub.docker.com/r/ukaea/paramak) + + +# Paramak + +The Paramak python package allows rapid production of 3D CAD models of fusion +reactors. The purpose of the Paramak is to provide geometry for parametric +studies. It is possible to use the created geometry in engineering and +neutronics studies as the STP or STL files produced can be automatically +converted to DAGMC compatible neutronics models or meshed and used in +finite element analysis codes. + +:point_right: [Documentation](https://paramak.readthedocs.io/en/main/) + +:point_right: [Video presentation](https://www.youtube.com/embed/XnuS9Ic1aWI) + +# History + +The package was originally conceived by Jonathan Shimwell and based on the +[FreeCAD Python API](https://wiki.freecadweb.org/FreeCAD_API). When +[CadQuery 2](https://github.com/CadQuery/cadquery) was released the project +started to migrate the code base. Shortly after this migration the project +became open-source and has flourished ever since. The project has grown largely +due to two contributors in particular (John Billingsley and +Remi Delaporte-Mathurin) and others have also helped, you can see all those who +have helped the development in the +[Authors.md](https://github.com/ukaea/paramak/blob/main/AUTHORS.md) and in the +[github contributions](https://github.com/ukaea/paramak/graphs/contributors). + + +## System Installation + +To install the Paramak you need to have +[Conda](https://docs.conda.io/projects/conda/en/latest/user-guide/install/), +[Cadquery 2](https://cadquery.readthedocs.io/en/latest/installation.html) and +[Pip](https://anaconda.org/anaconda/pip). If you have these three dependencies +already then you can install the Paramak using Pip: + +``` +pip install paramak +``` + +Detailed installation +[instructions](https://paramak.readthedocs.io/en/main/#prerequisites) +can be found in the User's Guide. + + +## Docker Image Installation + +Another option is to use the Docker image which contains all the required +dependencies. + +1. Install Docker CE for +[Ubuntu](https://docs.docker.com/install/linux/docker-ce/ubuntu/), +[Mac OS](https://store.docker.com/editions/community/docker-ce-desktop-mac), or +[Windows](https://hub.docker.com/editions/community/docker-ce-desktop-windows), +including the part where you enable docker use as a non-root user. + +2. Pull the docker image from the store by typing the following command in a +terminal window, or Windows users might prefer PowerShell. + + ```docker pull ukaea/paramak``` + +3. Now that you have the docker image you can enable graphics linking between +your os and docker, and then run the docker container by typing the following +commands in a terminal window. + + ```sudo docker run -p 8888:8888 ukaea/paramak /bin/bash -c "jupyter notebook --notebook-dir=/opt/notebooks --ip='*' --port=8888 --no-browser" --allow-root``` + +4. A URL should be displayed in the terminal and can now be opened in the +internet browser of your choice. This will load up the examples folder where +you can view the 3D objects created. Alternatively the Docker imag can be run +in terminal mode ```docker run -it ukaea/paramak``` + +Alternatively the Docker image can be run in terminal mode . +```docker run -it ukaea/paramak``` + +You may also want to make use of the +[--volume](https://docs.docker.com/storage/volumes/) +flag when running Docker so that you can retrieve files from the Docker +enviroment to your base system. + +Docker can also be used to run the tests using the command +```docker run -rm ukaea/parama pytest tests``` + +## Features + +In general the Paramak takes points and connection information in 2D space (XY) +and performs operations on them to create 3D volumes. The points and +connections can be provided by the user or when using parametric components +the points and connections are calculated by the software. + +Once points and connections between the points are provided the user has +options to perform CAD operations (rotate or extrude on different orientations) +to create a 3D volume and boolean operations like cut, union and intersection. + +The different families of shapes that can be made with the Paramak are shown in +the table below. The CadQuery objects created can be combined and modified +(e.g. fillet corners) using CadQueries powerful filtering capabilties to create +more complex models (e.g. a Tokamak). The Tokamak images below are coloured +based on the shape family that the component is made from. There are also +parametric components which provide convenient fusion relevant shapes for +common reactor components. +[](https://user-images.githubusercontent.com/8583900/94205189-a68f4200-feba-11ea-8c2d-789d1617ceea.png) + + +## Selection Of Parametric Reactors + +

+ + +

+ +

+ + +

+ +## Selection Of Parametric Components + +

+ +

+ + +## Selection Of Parametric Shapes + +| | Rotate | Extrude | Sweep | +|---------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------| +| Points connected with straight lines |

`RotateStraightShape()` |

`ExtrudeStraightShape()` |

`SweepStraightShape()` | +| Points connected with spline curves |

`RotateSplineShape()` |

`ExtrudeSplineShape()` |

`SweepSplineShape()` | +| Points connected with a mixture (splines, straights and circles) |

`RotateMixedShape()` |

`ExtrudeMixedShape()` |

`SweepMixedShape()` | +| Circular shapes |

`RotateCircleShape()` |

`ExtrudeCircleShape()` |

`SweepCircleShape()` | + +# Example Scripts + +There are several example scripts for making shapes, components, reactors and +neutronics models in the +[examples folder](https://github.com/ukaea/paramak/blob/main/examples/). +The following examples are minimal examples to demonstrate some basic usage. + + +## Usage - Parametric Shapes + +There are a collection of Python scripts in the example folder that demonstrate +simple shape construction and visualisation. However here is a quick example of +a RotateStraightShape. + +After importing the class the user then sets the points, by default, points +should be a list of (x,z) coordinates. In this case the points are connected +with straight lines. + +```python +import paramak + +my_shape = paramak.RotateStraightShape(points = [(20,0), (20,100), (100,0)]) +``` + +Once these properties have been set users can write 3D volumes in CAD STP or +STL formats. + +```python +my_shape.export_stp('example.stp') + +my_shape.export_stl('example.stl') +``` + +

+ + +## Usage - Parametric Components + +Parametric components are wrapped versions of the eight basic shapes where +parameters drive the construction of the shape. There are numerous parametric +components for a variety of different reactor components such as center columns, +blankets, poloidal field coils. This example shows the construction of a +plasma. Users could also construct a plasma by using a RotateSplineShape() +combined with coordinates for the points. However a parametric component called +Plasma can construct a plasma from more convenient parameters. Parametric +components also inherit from the Shape object so they have access to the same +methods like export_stp() and export_stl(). + +```python +import paramak + +my_plasma = paramak.Plasma(major_radius=620, minor_radius=210, triangularity=0.33, elongation=1.85) + +my_plasma.export_stp('plasma.stp') +``` + +

+ + +## Usage - Parametric Reactors + +Parametric Reactors are wrapped versions of a combination of parametric shapes +and components that comprise a particular reactor design. Some parametric +reactors include a ball reactor and a submersion ball reactor. These allow full +reactor models to be constructed by specifying a series of simple parameters. +This example shows the construction of a simple ball reactor without the +optional outer pf and tf coils. + +```python +import paramak + +my_reactor = paramak.BallReactor( + inner_bore_radial_thickness = 50, + inboard_tf_leg_radial_thickness = 50, + center_column_shield_radial_thickness= 50, + divertor_radial_thickness = 100, + inner_plasma_gap_radial_thickness = 50, + plasma_radial_thickness = 200, + outer_plasma_gap_radial_thickness = 50, + firstwall_radial_thickness = 50, + blanket_radial_thickness = 100, + blanket_rear_wall_radial_thickness = 50, + elongation = 2, + triangularity = 0.55, + number_of_tf_coils = 16, + rotation_angle = 180 + +my_reactor.name = 'BallReactor' + +my_reactor.export_stp() +``` + +

+ + +## Usage - Reactor Object + +A reactor object provides a container object for all Shape objects created, and +allows operations to be performed on the whole collection of Shapes. + +```python +import paramak +``` + +Initiate a Reactor object and pass a list of all Shape objects to the +shapes_and_components parameter. + +```python +my_reactor = paramak.Reactor(shapes_and_components = [my_shape, my_plasma]) +``` + +A html graph of the combined Shapes can be created. + +```python +my_reactor.export_html('reactor.html') +``` + +## Usage - Neutronics Model Creation + +It is possible to convert a parametric Reactor model into a neutronics model. + +To install two additional python packages needed to run neutronics with a +modified pip install + +```bash +pip install .[neutronics] +``` + +More information is avaialbe in the +[documentation](https://paramak.readthedocs.io/en/latest/paramak.parametric_neutronics.html#parametric-neutronics). + +There are several examples in the [examples folder](https://github.com/ukaea/paramak/blob/main/examples/https://github.com/ukaea/paramak/tree/main/examples/example_neutronics_simulations). + +To create the neutronics model you will need +[Trelis](https://www.coreform.com/products/trelis/) and the DAGMC plugin +installed [DAGMC plugin](https://github.com/svalinn/Trelis-plugin). + +Further information on DAGMC neutronics can be found +[here](https://svalinn.github.io/DAGMC/) and information on OpenMC can be found +[here](https://openmc.readthedocs.io/). The two codes can be used together to +simulate neutron transport on the h5m file created. The UKAEA openmc workshop +also has two tasks that might be of interest +[task 10](https://github.com/ukaea/openmc_workshop/tree/master/tasks/task_10) +and [task 12](https://github.com/ukaea/openmc_workshop/tree/master/tasks/task_12). diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 000000000..8f87c447d --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,29 @@ +# Docker +# Build a Docker image +# https://docs.microsoft.com/azure/devops/pipelines/languages/docker + +trigger: +- develop + +resources: +- repo: self + +variables: + tag: '$(Build.BuildId)' + +stages: +- stage: Build + displayName: Build image + jobs: + - job: Build + displayName: Build + pool: + vmImage: 'ubuntu-latest' + steps: + - task: Docker@2 + displayName: Build an image + inputs: + command: build + dockerfile: '$(Build.SourcesDirectory)/Dockerfile' + tags: | + $(tag) \ No newline at end of file diff --git a/bld.bat b/bld.bat new file mode 100644 index 000000000..602113031 --- /dev/null +++ b/bld.bat @@ -0,0 +1,2 @@ +"%PYTHON%" setup.py install +if errorlevel 1 exit 1 \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100644 index 000000000..fec5047cc --- /dev/null +++ b/build.sh @@ -0,0 +1 @@ +$PYTHON setup.py install # Python command to install the script. \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 000000000..269cadcf8 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/make_docs_from_rst.sh b/docs/make_docs_from_rst.sh new file mode 100644 index 000000000..c85d2f46f --- /dev/null +++ b/docs/make_docs_from_rst.sh @@ -0,0 +1,2 @@ +sphinx-build -b html ./source/ ./build/html/ +firefox ./build/html/index.html \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 000000000..08aa72300 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys + +sys.path.insert(0, os.path.abspath("../..")) +sys.path.insert(0, os.path.abspath("../../tests")) +sys.path.insert(0, os.path.abspath("../../examples")) + +# -- Project information ----------------------------------------------------- + +project = "Paramak" +copyright = "2020, UKAEA" +author = "The Paramak Development Team" + +# The short X.Y version +version = "" +# The full version, including alpha/beta/rc tags +release = "1.0" + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.coverage", + "sphinx.ext.napoleon"] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = ".rst" + +# The master toctree document. +master_doc = "index" + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + + +# shorten module names in readme +add_module_names = False + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "default" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = "Paramakdoc" + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, + "Paramak.tex", + "Paramak Documentation", + "John Billingsley", + "manual"), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [(master_doc, "paramak", "Paramak Documentation", [author], 1)] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + master_doc, + "Paramak", + "Paramak Documentation", + author, + "Paramak", + "One line description of project.", + "Miscellaneous", + ), +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ["search.html"] + + +# -- Extension configuration ------------------------------------------------- diff --git a/docs/source/example_neutronics_simulations.rst b/docs/source/example_neutronics_simulations.rst new file mode 100644 index 000000000..ad94c5251 --- /dev/null +++ b/docs/source/example_neutronics_simulations.rst @@ -0,0 +1,43 @@ +Examples - Neutronics Simulations +================================= + +There are minimal examples of neutronics simulations that just make a single +and there are slightly more involved examples that make more complex models or +a series of models. + +ball_reactor_minimal +^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: examples.example_neutronics_simulations.ball_reactor_minimal + :members: + :show-inheritance: + +`Link to script `_ + + +center_column_study_reactor_minimal +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: examples.example_neutronics_simulations.center_column_study_reactor_minimal + :members: + :show-inheritance: + +`Link to script `_ + +ball_reactor +^^^^^^^^^^^^ + +.. automodule:: examples.example_neutronics_simulations.ball_reactor + :members: + :show-inheritance: + +`Link to script `_ + +center_column_study_reactor +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: examples.example_neutronics_simulations.center_column_study_reactor + :members: + :show-inheritance: + +`Link to script `_ diff --git a/docs/source/example_parametric_components.rst b/docs/source/example_parametric_components.rst new file mode 100644 index 000000000..fa7e60912 --- /dev/null +++ b/docs/source/example_parametric_components.rst @@ -0,0 +1,70 @@ +Examples - Parametric Components +================================ + +make_all_parametric_components.py +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/8583900/98823600-387eea00-242a-11eb-9fe3-df65aaa3dd21.png + :width: 713 + :align: center + +.. automodule:: examples.example_parametric_components.make_all_parametric_components + :members: + :show-inheritance: + +`Link to script `_ + +make_plasmas.py +^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/8583900/93624384-2e1b1380-f9d8-11ea-99d1-9bf9e4e5b838.png + :width: 1050 + :height: 700 + :align: center + +.. automodule:: examples.example_parametric_components.make_plasmas + :members: + :show-inheritance: + +`Link to script `_ + +make_demo_style_blankets.py +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/8583900/93619812-02e0f600-f9d1-11ea-903c-913c8bcb0f1b.png + :width: 1050 + :height: 350 + :align: center + +.. automodule:: examples.example_parametric_components.make_demo_style_blankets + :members: + :show-inheritance: + +`Link to script `_ + +make_firstwall_for_neutron_wall_loading.py +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/8583900/93807581-bc92cd80-fc42-11ea-8522-7fe14287b3c4.png + :width: 437 + :height: 807 + :align: center + +.. automodule:: examples.example_parametric_components.make_firstwall_for_neutron_wall_loading + :members: + :show-inheritance: + +`Link to script `_ + +make_magnet_set +^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/8583900/99276201-5088ac00-2824-11eb-9927-a7ea1094b1e5.png + :width: 500 + :align: center + +.. automodule:: examples.example_parametric_components.make_magnet_set + :members: + :show-inheritance: + +`Link to script `_ diff --git a/docs/source/example_parametric_reactors.rst b/docs/source/example_parametric_reactors.rst new file mode 100644 index 000000000..7e094584d --- /dev/null +++ b/docs/source/example_parametric_reactors.rst @@ -0,0 +1,70 @@ +Examples - Parametric Reactors +============================== + +ball_reactor.py +^^^^^^^^^^^^^^^ + +.. automodule:: examples.example_parametric_reactors.ball_reactor + :members: + :show-inheritance: + +`Link to script `_ + +ball_reactor_single_null.py +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: examples.example_parametric_reactors.ball_reactor_single_null + :members: + :show-inheritance: + +`Link to script `_ + +segmented_blanket_ball_reactor.py +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: examples.example_parametric_reactors.segmented_blanket_ball_reactor + :members: + :show-inheritance: + +`Link to script `_ + +submersion_reactor.py +^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: examples.example_parametric_reactors.submersion_reactor + :members: + :show-inheritance: + +`Link to script `_ + +submersion_reactor_single_null.py +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: examples.example_parametric_reactors.submersion_reactor_single_null + :members: + :show-inheritance: + +`Link to script `_ + +center_column_study_reactor.py +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: examples.example_parametric_reactors.center_column_study_reactor + :members: + :show-inheritance: + +`Link to script `_ + +htc_reactor.py +^^^^^^^^^^^^^^ + +.. automodule:: examples.example_parametric_reactors.htc_reactor + :members: + :show-inheritance: + +|htc_reactor_stp| + +.. |htc_reactor_stp| image:: https://user-images.githubusercontent.com/8583900/100032191-5ae01280-2def-11eb-9654-47c3869b3a2c.png + :width: 700 + +`Link to script `_ diff --git a/docs/source/example_parametric_shapes.rst b/docs/source/example_parametric_shapes.rst new file mode 100644 index 000000000..6d1f40efd --- /dev/null +++ b/docs/source/example_parametric_shapes.rst @@ -0,0 +1,75 @@ +Examples - Parametric Shapes +============================ + +make_CAD_from_points.py +^^^^^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/56687624/88064585-641c5280-cb63-11ea-97b1-1b7dcfabc07c.gif + :width: 450 + :height: 275 + :align: center + +.. automodule:: examples.example_parametric_shapes.make_CAD_from_points + :members: + :show-inheritance: + +`Link to script `_ + + +make_blanket_from_points.py +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/56687624/87058930-998a7d00-c200-11ea-846e-4084dbf82748.png + :width: 400 + :height: 400 + :align: center + +.. automodule:: examples.example_parametric_shapes.make_blanket_from_points + :members: + :show-inheritance: + +`Link to script `_ + + +make_blanket_from_parameters.py +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/56687624/87058944-9e4f3100-c200-11ea-8bd3-669b3705c179.png + :width: 400 + :height: 400 + :align: center + +.. automodule:: examples.example_parametric_shapes.make_blanket_from_parameters + :members: + :show-inheritance: + +`Link to script `_ + +make_can_reactor_from_points.py +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/56687624/87060447-74970980-c202-11ea-8720-403c24dbabcc.gif + :width: 1300 + :height: 450 + :align: center + +.. automodule:: examples.example_parametric_shapes.make_can_reactor_from_points + :members: + :show-inheritance: + +`Link to script `_ + + +make_can_reactor_from_parameters.py +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/56687624/87060447-74970980-c202-11ea-8720-403c24dbabcc.gif + :width: 1300 + :height: 450 + :align: center + +.. automodule:: examples.example_parametric_shapes.make_can_reactor_from_parameters + :members: + :show-inheritance: + +`Link to script `_ diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 000000000..c9432b2d8 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,461 @@ +Paramak +======= + +The Paramak python package allows rapid production of 3D CAD models of fusion +reactors. The purpose of the Paramak is to provide geometry for parametric +studies. It is possible to use the created geometry in engineering and +neutronics studies as the STP files produced can be automatically converted to +DAGMC compatible neutronics models or meshed and used in finite element +analysis codes. + +Features have been added to address particular needs and the software is by no +means a finished product. Contributions are welcome. CadQuery functions provide +the majority of the features, and incorporating additional capabilities is +straightforward for developers with Python knowledge. + +.. raw:: html + +
+ +
+ +.. toctree:: + :maxdepth: 1 + + paramak.parametric_shapes + paramak.parametric_components + paramak.parametric_reactors + paramak.parametric_neutronics + paramak.core_modules + example_parametric_shapes + example_parametric_components + example_parametric_reactors + example_neutronics_simulations + tests + +Prerequisites +------------- + +To use the paramak tool you will need Python 3 and Cadquery 2.0 or newer +installed. + +* `Python 3 `_ + +* `CadQuery 2.0 `_ + +Cadquery 2.0 must be installed in a conda environment via conda-forge. +Conda environments are activated using Anaconda or Miniconda. + +* `Anaconda `_ +* `Miniconda `_ + +Once you have activated a conda environment, Cadquery 2.0 can be installed +using the command: + +.. code-block:: python + + conda install -c conda-forge -c cadquery cadquery=2 + +A more detailed description of installing Cadquery 2.0 can be found here: + +* `Cadquery 2.0 installation `_ + + +System Installation +------------------- + +The quickest way to install the Paramak is to use pip. In the terminal type... + +.. code-block:: bash + + pip install paramak + +Alternatively you can download the repository using the `download link `_ or clone the repository using: + +.. code-block:: bash + + git clone https://github.com/Shimwell/paramak.git + +Navigate to the paramak repository and within the terminal install the paramak +package and the dependencies using pip3. + +.. code-block:: bash + + pip install . + +Alternatively you can install the paramak with the following command. + +.. code-block:: bash + + python setup.py install + +You can also install optional dependencies that add some neutronics +capabilities to the paramak. This will install neutronics_material_maker and +parametric_plasma_source. In addition to this you would need DAGMC, OpenMC and +a method of imprinting and merging. +`More details `_ + + +.. code-block:: bash + + pip install .[neutronics] + + +Docker Image Installation +------------------------- + +Another option is to use the Docker image which contains all the required +dependencies. + +1. Install Docker CE for `Ubuntu `_ , +`Mac OS `_ or +`Windows `_ +including the part where you enable docker use as a non-root user. + +2. Pull the docker image from the store by typing the following command in a +terminal window, or Windows users might prefer PowerShell. + +.. code-block:: bash + + docker pull ukaea/paramak + +3. Now that you have the docker image you can enable graphics linking between +your os and docker, and then run the docker container by typing the following +commands in a terminal window. + +.. code-block:: bash + + sudo docker run -p 8888:8888 ukaea/paramak /bin/bash -c "jupyter notebook --notebook-dir=/opt/notebooks --ip='*' --port=8888 --no-browser --allow-root" + +4. A URL should be displayed in the terminal and can now be opened in the +internet browser of your choice. This will load up the examples folder where +you can view the 3D objects created. + +Alternatively the Docker image can be run in terminal mode . + +.. code-block:: bash + + docker run -it ukaea/paramak + +You may also want to make use of the +`--volume `_ +flag when running Docker so that you can retrieve files from the Docker +enviroment to your base system. + +Presentations +------------- + +Currently we just have one presentation that covers the Paramak. + +`Link to presentation `_ + + +Features +-------- + +In general the Paramak takes input arguments and creates 3D objects. This can +be accomplished via the use of parametric Shapes, parametric Components and +parametric Reactors with each level building upon the level below. + +Parametric Shapes are the simplest and accept points and connection information +in 2D space (defaults to x,z) and performs operations on them to create 3D +volumes. The points and connections are provided by the user when making +parametric Shapes. Supported CAD opperations include (rotate, extrude, sweep) +and Boolean opperations such as cut, union and intersect. Additionally the +CadQuery objects created can be combined and modified using CadQuery's powerful +filtering capabilties to furter customise the shapes by performing operations +like edge filleting. + +Parametric Components build on top of this foundation and will calculate the +points and connections for you when provided with input arguments. The inputs +differ between components as a center column requires different inputs to a +breeder blanket or a magnet. + +Parametric Reactors build upon these two lower level objects to create an +entire reactor model from input parameters. Linkage between the componets is +encoded in each parametric Ractor design. + +The different parametric reactor families are shown below. + +.. image:: https://user-images.githubusercontent.com/8583900/99137324-fddfa200-2621-11eb-9063-f5f7f60ddd8d.png + :width: 713 + :align: center + +The different parametric Components are shown below. + +.. image:: https://user-images.githubusercontent.com/8583900/98823600-387eea00-242a-11eb-9fe3-df65aaa3dd21.png + :width: 713 + :height: 245 + :align: center + +The different families of parametric Shapes that can be made with the Paramak +are shown int he table below. + + + +.. |rotatestraight| image:: https://user-images.githubusercontent.com/56687624/87055469-4f070180-c1fc-11ea-9679-a29e37a90e15.png + :height: 120px + +.. |extrudestraight| image:: https://user-images.githubusercontent.com/56687624/87055493-56c6a600-c1fc-11ea-8c58-f5b62ae72e0e.png + :height: 120px + +.. |sweepstraight| image:: https://user-images.githubusercontent.com/56687624/98713447-8c80c480-237f-11eb-8615-c090e93138f6.png + :height: 120px + +.. |rotatespline| image:: https://user-images.githubusercontent.com/56687624/87055473-50382e80-c1fc-11ea-95dd-b4932b1e78d9.png + :height: 120px + +.. |extrudespline| image:: https://user-images.githubusercontent.com/56687624/98713431-87bc1080-237f-11eb-9075-01bca99b7018.png + :height: 120px + +.. |sweepspline| image:: https://user-images.githubusercontent.com/56687624/98713443-8b4f9780-237f-11eb-83bb-38ca7f222073.png + :height: 120px + +.. |rotatecircle| image:: https://user-images.githubusercontent.com/56687624/98713427-868ae380-237f-11eb-87af-cf6b5fe032b2.png + :height: 120px + +.. |extrudecircle| image:: https://user-images.githubusercontent.com/56687624/87055517-5b8b5a00-c1fc-11ea-83ef-d4329c6815f7.png + :height: 120px + +.. |sweepcircle| image:: https://user-images.githubusercontent.com/56687624/98713436-88ed3d80-237f-11eb-99cd-27dcb4f313b1.png + :height: 120px + +.. |rotatemixed| image:: https://user-images.githubusercontent.com/56687624/87055483-53cbb580-c1fc-11ea-878d-92835684c8ff.png + :height: 120px + +.. |extrudemixed| image:: https://user-images.githubusercontent.com/56687624/87055511-59c19680-c1fc-11ea-8740-8c7987745c45.png + :height: 120px + +.. |sweepmixed| image:: https://user-images.githubusercontent.com/56687624/98713440-8a1e6a80-237f-11eb-9eed-12b9d7731090.png + :height: 120px + + + + + + ++--------------------------------------+--------------------------------------+---------------------------------------+---------------------------------------+ +| | Rotate | Extrude | Sweep | ++======================================+======================================+=======================================+=======================================+ +| Points connected with straight lines | |rotatestraight| | |extrudestraight| | |sweepstraight| | +| | | | | +| | | | | +| | | | | +| | | | | +| | :: | :: | :: | +| | | | | +| | RotateStraightShape() | ExtrudeStraightShape() | SweepStraightShape() | ++--------------------------------------+--------------------------------------+---------------------------------------+---------------------------------------+ +| Points connected with spline curves | |rotatespline| | |extrudespline| | |sweepspline| | +| | | | | +| | | | | +| | | | | +| | | | | +| | :: | :: | :: | +| | | | | +| | RotateSplineShape() | ExtrudeSplineShape() | SweepSplineShape() | ++--------------------------------------+--------------------------------------+---------------------------------------+---------------------------------------+ +| Points connected with a circle | |rotatecircle| | |extrudecircle| | |sweepcircle| | +| | | | | +| | | | | +| | | | | +| | | | | +| | :: | :: | :: | +| | | | | +| | RotateCircleShape() | ExtrudeCircleShape() | SweepCircleShape() | ++--------------------------------------+--------------------------------------+---------------------------------------+---------------------------------------+ +| Points connected with a mixture | |rotatemixed| | |extrudemixed| | |sweepmixed| | +| | | | | +| :: | | | | +| | | | | +| (splines, straights and circles) | | | | +| | :: | :: | :: | +| | | | | +| | RotateMixedShape() | ExtrudeMixedShape() | SweepMixedShape() | ++--------------------------------------+--------------------------------------+---------------------------------------+---------------------------------------+ + + +Usage - Parametric Shapes +------------------------- + +There are a collection of Python scripts in the example folder that demonstrate +simple shape construction and visualisation. However here is a quick example of +a RotateStraightShape. + +After importing the class the user then sets the points. By default, points +should be a list of (x,z) points. In this case the points are connected with +straight lines. + +.. code-block:: python + + import paramak + + my_shape = paramak.RotateStraightShape(points = [(20,0), (20,100), (100,0)]) + +Once these properties have been set then users can write 3D volumes in CAD STP +or STL formats. + +.. code-block:: python + + my_shape.export_stp('example.stp') + + my_shape.export_stl('example.stl') + +.. image:: https://user-images.githubusercontent.com/56687624/88935761-ff0ae000-d279-11ea-8848-de9b486840d9.png + :width: 350 + :height: 300 + :align: center + +Usage - Parametric Components +----------------------------- + +Parametric components are wrapped versions of the eight basic shapes where +parameters drive the construction of the shape. There are numerous parametric +components for a variety of different reactor components such as center columns, +blankets, poloidal field coils. This example shows the construction of a +plasma. Users could also construct a plasma by using a RotateSplineShape() +combined with coordinates for the points. However a parametric component called +Plasma can construct a plasma from more convenient parameters. Parametric +components also inherit from the Shape object so they have access to the same +methods like export_stp() and export_stl(). + +.. code-block:: python + + import paramak + + my_plasma = paramak.Plasma(major_radius=620, minor_radius=210, triangularity=0.33, elongation=1.85) + + my_plasma.export_stp('plasma.stp') + +.. image:: https://user-images.githubusercontent.com/56687624/88935871-1ea20880-d27a-11ea-82e1-1afa55ff9ba8.png + :width: 350 + :height: 300 + :align: center + +Usage - Parametric Reactors +--------------------------- + +Parametric Reactors() are wrapped versions of a combination of parametric +shapes and components that comprise a particular reactor design. Some +parametric reactors include a ball reactor and a submersion ball reactor. These +allow full reactor models to be constructed by specifying a series of simple +parameters. This example shows the construction of a simple ball reactor +without the optional outer pf and tf coils. + +.. code-block:: python + + import paramak + + my_reactor = paramak.BallReactor( + inner_bore_radial_thickness = 50, + inboard_tf_leg_radial_thickness = 50, + center_column_shield_radial_thickness= 50, + divertor_radial_thickness = 100, + inner_plasma_gap_radial_thickness = 50, + plasma_radial_thickness = 200, + outer_plasma_gap_radial_thickness = 50, + firstwall_radial_thickness = 50, + blanket_radial_thickness = 100, + blanket_rear_wall_radial_thickness = 50, + elongation = 2, + triangularity = 0.55, + number_of_tf_coils = 16, + rotation_angle = 180 + ) + + my_reactor.name = 'BallReactor' + + my_reactor.export_stp() + +.. image:: https://user-images.githubusercontent.com/56687624/89203299-465fdc00-d5ac-11ea-8663-a5b7eecfb584.png + :width: 350 + :height: 300 + :align: center + +Usage - Reactor Object +---------------------- + +A reactor object provides a container object for all Shape objects created, and +allows operations to be performed on the whole collection of Shapes. + +.. code-block:: python + + import paramak + +Initiate a Reactor object and pass a list of all Shape objects to the +shapes_and_components parameter. + +.. code-block:: python + + my_reactor = paramak.Reactor(shapes_and_components = [my_shape, my_plasma]) + +A html graph of the combined Shapes can be created. + +.. code-block:: python + + my_reactor.export_html('reactor.html') + + +Usage - Neutronics Model Creation +--------------------------------- + +First assign stp_filenames to each of the Shape objects that were created +earlier on. + +.. code-block:: python + + my_shape.stp_filename = 'my_shape.stp' + + my_plasma.stp_filename = 'my_plasma.stp' + +Then assign material_tags to each of the Shape objects. + +.. code-block:: python + + my_shape.material_tag = 'steel' + + my_plasma.material_tag = 'DT_plasma' + +Note - Tetrahedral meshes can also be assigned to Shape objects. + +Now add the Shape objects to a freshly created reactor object. + +.. code-block:: python + + new_reactor = Reactor([my_shape, my_plasma]) + +The entire reactor can now be exported as step files. This also generates a +DAGMC graveyard automatically. + +.. code-block:: python + + my_reactor.export_stp() + +A manifest.json file that contains all the step filenames and materials can now +be created. + +.. code-block:: python + + my_reactor.export_neutronics_description() + +Once you step files and the neutronics description has been exported then `Trelis `_ can be used to generate a DAGMC geometry in the usual manner. There is also a convenient script included in task 12 of the UKAEA openmc workshop which can be used in conjunction with the neutronics description json file to automatically create a DAGMC geometry. Download `this script `_ and place it in the same directory as the manifest.json and step files. Then run the following command from the terminal. You will need to have previously installed the `DAGMC plugin `_ for Trelis. + +:: + + trelis make_faceteted_neutronics_model.py + +Alternatively, run this without the GUI in batch mode using: + +:: + + trelis -batch -nographics make_faceteted_neutronics_model.py + +This should export a h5m file for use in DAGMC. + +Further information on DAGMC neutronics can be found `here `_ and information on OpenMC can be found `here `_ . The two codes can be used together to simulate neutron transport on the h5m file created. The UKAEA openmc workshop also has two tasks that might be of interest `task 10 `_ and `task 12 `_ . + + +Example Scripts +--------------- + +There are several example scripts in the `examples folder `_ . A good one to start with is `make_CAD_from_points `_ which makes simple examples of the different types of shapes (extrude, rotate) with different connection methods (splines, straight lines and circles). diff --git a/docs/source/paramak.core_modules.rst b/docs/source/paramak.core_modules.rst new file mode 100644 index 000000000..6443dd842 --- /dev/null +++ b/docs/source/paramak.core_modules.rst @@ -0,0 +1,31 @@ + +Core Modules +============ + +These modules form the basis of parametric_shapes, +parametric_components and parametric_reactors. They provide +underlying functionality common to all classes that inherit +from them. + +Shape() +^^^^^^^ + +.. automodule:: paramak.shape + :members: + :show-inheritance: + +Reactor() +^^^^^^^^^ + +.. automodule:: paramak.reactor + :members: + :show-inheritance: + +utils +^^^^^ + +Utility functions used by several parametric shapes, components and reactors. + +.. automodule:: paramak.utils + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/paramak.parametric_components.rst b/docs/source/paramak.parametric_components.rst new file mode 100644 index 000000000..0fe6c2ae8 --- /dev/null +++ b/docs/source/paramak.parametric_components.rst @@ -0,0 +1,555 @@ +Parametric Components +===================== + +These are components that represent a selection of the components found in fusion +reactors and are created from parameters. These components all inherit from the +parametric Shape classes. + +BlanketConstantThicknessArcH() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +|BlanketConstantThicknessArcHstp| |BlanketConstantThicknessArcHsvg| + +.. |BlanketConstantThicknessArcHstp| image:: https://user-images.githubusercontent.com/8583900/86519778-32eb1500-be36-11ea-9794-0383b66624c5.png + :width: 250 +.. |BlanketConstantThicknessArcHsvg| image:: https://user-images.githubusercontent.com/56687624/88293663-38c86d80-ccf3-11ea-9bfa-c166fc99c52c.png + :width: 300 + +.. automodule:: paramak.parametric_components.blanket_constant_thickness_arc_h + :members: + :show-inheritance: + +BlanketConstantThicknessArcV() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +|BlanketConstantThicknessArcVstp| |BlanketConstantThicknessArcVsvg| + +.. |BlanketConstantThicknessArcVstp| image:: https://user-images.githubusercontent.com/8583900/86365020-dee30380-bc70-11ea-8258-5e591c6c3235.png + :width: 250 +.. |BlanketConstantThicknessArcVsvg| image:: https://user-images.githubusercontent.com/56687624/88293666-39f99a80-ccf3-11ea-8c8d-84275fd0e0ce.png + :width: 350 + +.. automodule:: paramak.parametric_components.blanket_constant_thickness_arc_v + :members: + :show-inheritance: + + +BlanketCutterParallels() +^^^^^^^^^^^^^^^^^^^^^^^^ + +|BlanketCutterParallelsstp| |BlanketCutterParallelssvg| + +.. |BlanketCutterParallelsstp| image:: https://user-images.githubusercontent.com/8583900/97328431-eb1d4d00-186d-11eb-9f5d-4bbee9e3b17d.png + :width: 250 +.. |BlanketCutterParallelssvg| image:: https://user-images.githubusercontent.com/8583900/97329670-32580d80-186f-11eb-8b1a-b7712ddb0e83.png + :width: 400 + +.. automodule:: paramak.parametric_components.blanket_cutter_parallels + :members: + :show-inheritance: + + +BlanketCutterStar() +^^^^^^^^^^^^^^^^^^^ + +|BlanketCutterStarstp| |BlanketCutterStarsvg| + +.. |BlanketCutterStarstp| image:: https://user-images.githubusercontent.com/8583900/97103699-0178ac80-16a6-11eb-8e5a-ec3575d265fe.png + :width: 250 +.. |BlanketCutterStarsvg| image:: https://user-images.githubusercontent.com/8583900/97103794-b0b58380-16a6-11eb-86f0-fb5530d630af.png + :width: 400 + +.. automodule:: paramak.parametric_components.blanket_cutters_star + :members: + :show-inheritance: + + +BlanketFP() +^^^^^^^^^^^ + +|BlanketFPstp| |BlanketFPsvg| + +.. |BlanketFPstp| image:: https://user-images.githubusercontent.com/8583900/87254778-fe520b80-c47c-11ea-845f-470991d74874.png + :width: 220 +.. |BlanketFPsvg| image:: https://user-images.githubusercontent.com/8583900/94867319-f0d36e80-0438-11eb-8516-7b8f2a7cc7ee.png + :width: 350 + +.. automodule:: paramak.parametric_components.blanket_fp + :members: + :show-inheritance: + + +BlanketFPPoloidalSegments() +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +|BlanketFPPoloidalSegmentsstp| |BlanketFPPoloidalSegmentssvg| + +.. |BlanketFPPoloidalSegmentsstp| image:: https://user-images.githubusercontent.com/8583900/98735027-af6ca200-239a-11eb-9a59-4a570f91a1fc.png + :width: 220 +.. |BlanketFPPoloidalSegmentssvg| image:: https://user-images.githubusercontent.com/8583900/98870151-ca0e4c00-246a-11eb-8a37-e7620344d8c1.png + :width: 350 + +.. automodule:: paramak.parametric_components.blanket_poloidal_segment + :members: + :show-inheritance: + + +CenterColumnShieldCylinder() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +|CenterColumnShieldCylinderstp| |CenterColumnShieldCylindersvg| + +.. |CenterColumnShieldCylinderstp| image:: https://user-images.githubusercontent.com/56687624/86241438-caccd280-bb9a-11ea-9548-b199759a6dbc.png + :width: 160px +.. |CenterColumnShieldCylindersvg| image:: https://user-images.githubusercontent.com/56687624/88293674-3c5bf480-ccf3-11ea-8197-8db75358ff36.png + :width: 370px + +.. automodule:: paramak.parametric_components.center_column_cylinder + :members: + :show-inheritance: + +CenterColumnShieldHyperbola() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +|CenterColumnShieldHyperbolastp| |CenterColumnShieldHyperbolasvg| + +.. |CenterColumnShieldHyperbolastp| image:: https://user-images.githubusercontent.com/56687624/86241456-d0c2b380-bb9a-11ea-9728-88fe4081345f.png + :width: 170px +.. |CenterColumnShieldHyperbolasvg| image:: https://user-images.githubusercontent.com/56687624/88293672-3b2ac780-ccf3-11ea-9907-b1c8fd1ba0f0.png + :width: 410px + +.. automodule:: paramak.parametric_components.center_column_hyperbola + :members: + :show-inheritance: + +CenterColumnShieldFlatTopHyperbola() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +|CenterColumnShieldFlatTopHyperbolastp| |CenterColumnShieldFlatTopHyperbolasvg| + +.. |CenterColumnShieldFlatTopHyperbolastp| image:: https://user-images.githubusercontent.com/56687624/86241446-cdc7c300-bb9a-11ea-8310-d54397338da8.png + :width: 170px +.. |CenterColumnShieldFlatTopHyperbolasvg| image:: https://user-images.githubusercontent.com/56687624/88293680-3ebe4e80-ccf3-11ea-8603-b7a290e6bfb4.png + :width: 370px + +.. automodule:: paramak.parametric_components.center_column_flat_top_hyperbola + :members: + :show-inheritance: + +CenterColumnShieldFlatTopCircular() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +|CenterColumnShieldFlatTopCircularstp| |CenterColumnShieldFlatTopCircularsvg| + +.. |CenterColumnShieldFlatTopCircularstp| image:: https://user-images.githubusercontent.com/56687624/86241446-cdc7c300-bb9a-11ea-8310-d54397338da8.png + :width: 170px +.. |CenterColumnShieldFlatTopCircularsvg| image:: https://user-images.githubusercontent.com/56687624/88293678-3d8d2180-ccf3-11ea-97f7-da9a46beddbf.png + :width: 370px + +.. automodule:: paramak.parametric_components.center_column_flat_top_circular + :members: + :show-inheritance: + +CenterColumnShieldPlasmaHyperbola() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/56687624/86241464-d3bda400-bb9a-11ea-83b4-a3ff0bf630c4.png + :width: 180px + :align: center + +.. automodule:: paramak.parametric_components.center_column_plasma_dependant + :members: + :show-inheritance: + +CuttingWedge() +^^^^^^^^^^^^^^ + +|CuttingWedgestp| |CuttingWedgesvg| + +.. |CuttingWedgestp| image:: https://user-images.githubusercontent.com/8583900/94726081-a678c180-0354-11eb-93f2-98d4b4a6839e.png + :width: 300px +.. |CuttingWedgesvg| image:: https://user-images.githubusercontent.com/8583900/94726514-433b5f00-0355-11eb-94d2-06b2bba1ed4a.png + :width: 300px + +.. automodule:: paramak.parametric_components.cutting_wedge + :members: + :show-inheritance: + +CuttingWedgeFS() +^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/8583900/94726081-a678c180-0354-11eb-93f2-98d4b4a6839e.png + :width: 300px + +.. automodule:: paramak.parametric_components.cutting_wedge_fs + :members: + :show-inheritance: + +InboardFirstwallFCCS() +^^^^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/8583900/94197757-219e2b80-feae-11ea-8e41-0786d56c8b66.png + :width: 786px + :align: center + +.. automodule:: paramak.parametric_components.inboard_firstwall_fccs + :members: + :show-inheritance: + +ITERtypeDivertor() +^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/40028739/88180936-626b9100-cc2e-11ea-92df-1bac68b11e3b.png + :width: 250px + :align: center + +.. automodule:: paramak.parametric_components.divertor_ITER +.. autoclass:: ITERtypeDivertor + :members: + :show-inheritance: + +ITERtypeDivertorNoDome() +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/56687624/91977407-871d5300-ed1a-11ea-91e5-922e5c9b31a0.png + :width: 250px + :align: center + +.. automodule:: paramak.parametric_components.divertor_ITER_no_dome +.. autoclass:: ITERtypeDivertorNoDome + :members: + :show-inheritance: + +InnerTfCoilsCircular() +^^^^^^^^^^^^^^^^^^^^^^ + +|InnerTfCoilsCircularstp| |InnerTfCoilsCircularsvg| + +.. |InnerTfCoilsCircularstp| image:: https://user-images.githubusercontent.com/56687624/86241469-d9b38500-bb9a-11ea-935f-8644fa01ab8c.png + :width: 210px +.. |InnerTFCoilsCircularsvg| image:: https://user-images.githubusercontent.com/56687624/88293695-41b93f00-ccf3-11ea-9ea8-338a64bb5566.png + :width: 390px + +.. automodule:: paramak.parametric_components.inner_tf_coils_circular + :members: + :show-inheritance: + +InnerTfCoilsFlat() +^^^^^^^^^^^^^^^^^^ + +|InnerTfCoilsFlatstp| |InnerTfCoilsFlatsvg| + +.. |InnerTfCoilsFlatstp| image:: https://user-images.githubusercontent.com/56687624/86241472-db7d4880-bb9a-11ea-8fb9-325b3342fe11.png + :width: 210px +.. |InnerTfCoilsFlatsvg| image:: https://user-images.githubusercontent.com/56687624/88293697-42ea6c00-ccf3-11ea-9e92-dc698813f1ee.png + :width: 390px + +.. automodule:: paramak.parametric_components.inner_tf_coils_flat + :members: + :show-inheritance: + +PoloidalFieldCoil() +^^^^^^^^^^^^^^^^^^^ + +|PoloidalFieldCoilstp| |PoloidalFieldCoilsvg| + +.. |PoloidalFieldCoilstp| image:: https://user-images.githubusercontent.com/56687624/86241487-dfa96600-bb9a-11ea-96ba-54f22ecef1ef.png + :width: 330px +.. |PoloidalFieldCoilsvg| image:: https://user-images.githubusercontent.com/8583900/94807412-86461280-03e7-11eb-9854-ecf66489c262.png + :width: 360px + +.. automodule:: paramak.parametric_components.poloidal_field_coil + :members: + :show-inheritance: + +PoloidalFieldCoilFP() +^^^^^^^^^^^^^^^^^^^^^ + +|PoloidalFieldCoilFPstp| |PoloidalFieldCoilFPsvg| + +.. |PoloidalFieldCoilFPstp| image:: https://user-images.githubusercontent.com/56687624/86241487-dfa96600-bb9a-11ea-96ba-54f22ecef1ef.png + :width: 330px +.. |PoloidalFieldCoilFPsvg| image:: https://user-images.githubusercontent.com/8583900/95579521-ba47b600-0a2d-11eb-9bdf-7f0415396978.png + :width: 360px + +.. automodule:: paramak.parametric_components.poloidal_field_coil_fp + :members: + :show-inheritance: + +PoloidalFieldCoilSet() +^^^^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/8583900/93832861-eb269d80-fc6e-11ea-861c-45de724478a8.png + :width: 395px + :align: center + +.. automodule:: paramak.parametric_components.poloidal_field_coil_set + :members: + :show-inheritance: + +PoloidalFieldCoilCase() +^^^^^^^^^^^^^^^^^^^^^^^ + +|PoloidalFieldCoilCasestp| |PoloidalFieldCoilCasesvg| + +.. |PoloidalFieldCoilCasestp| image:: https://user-images.githubusercontent.com/56687624/86241492-e1732980-bb9a-11ea-9331-586a39d32cfb.png + :width: 300px +.. |PoloidalFieldCoilCasesvg| image:: https://user-images.githubusercontent.com/8583900/94807553-bab9ce80-03e7-11eb-9a2a-1b78a780b049.png + :width: 370px + +.. automodule:: paramak.parametric_components.poloidal_field_coil_case + :members: + :show-inheritance: + +PoloidalFieldCoilCaseFC() +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/56687624/86241492-e1732980-bb9a-11ea-9331-586a39d32cfb.png + :width: 220px + :align: center + +.. automodule:: paramak.parametric_components.poloidal_field_coil_case_fc + :members: + :show-inheritance: + +PoloidalFieldCoilCaseSet() +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/8583900/93908750-e86f8b00-fcf6-11ea-938e-349dd09e5915.png + :width: 586px + :align: center + +.. automodule:: paramak.parametric_components.poloidal_field_coil_case_set + :members: + :show-inheritance: + +PoloidalFieldCoilCaseSetFC() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/8583900/93908750-e86f8b00-fcf6-11ea-938e-349dd09e5915.png + :width: 586px + :align: center + +.. automodule:: paramak.parametric_components.poloidal_field_coil_case_set_fc + :members: + :show-inheritance: + +PoloidalSegmenter() +^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/8583900/93811079-84da5480-fc47-11ea-9c6c-7fd132f6d72d.png + :width: 605px + :align: center + +.. automodule:: paramak.parametric_components.poloidal_segmenter + :members: + :show-inheritance: + +PortCutterRotated() +^^^^^^^^^^^^^^^^^^^ + +|PortCutterRotatedstp| |PortCutterRotatedsvg| + +.. |PortCutterRotatedstp| image:: https://user-images.githubusercontent.com/8583900/95115392-511a2700-073d-11eb-9cb9-d6d2bec80e2c.png + :width: 300px +.. |PortCutterRotatedsvg| image:: https://user-images.githubusercontent.com/8583900/95115923-267c9e00-073e-11eb-898b-bafbb2626b02.png + :width: 380px + +.. automodule:: paramak.parametric_components.port_cutters_rotated + :members: + :show-inheritance: + +PortCutterRectangular() +^^^^^^^^^^^^^^^^^^^^^^^ + +|PortCutterRectangularstp| |PortCutterRectangularsvg| + +.. |PortCutterRectangularstp| image:: https://user-images.githubusercontent.com/8583900/95790579-8f808a80-0cd7-11eb-83e1-872a98fe0bc8.png + :width: 300px +.. |PortCutterRectangularsvg| image:: https://user-images.githubusercontent.com/8583900/99831528-1fc3b200-2b57-11eb-9b73-8efab06cf3ef.png + :width: 300px + +.. automodule:: paramak.parametric_components.port_cutters_rectangular + :members: + :show-inheritance: + +PortCutterCircular() +^^^^^^^^^^^^^^^^^^^^ + +|PortCutterCircularstp| |PortCutterCircularsvg| + +.. |PortCutterCircularstp| image:: https://user-images.githubusercontent.com/8583900/95790580-90b1b780-0cd7-11eb-944f-14fe290f8442.png + :width: 300px +.. |PortCutterCircularsvg| image:: https://user-images.githubusercontent.com/8583900/99830949-53eaa300-2b56-11eb-886e-d5ee04c85b4a.png + :width: 300px + +.. automodule:: paramak.parametric_components.port_cutters_circular + :members: + :show-inheritance: + +Plasma() +^^^^^^^^ + +|Plasmastp| |Plasmasvg| + +.. |Plasmastp| image:: https://user-images.githubusercontent.com/8583900/87316638-f39b8300-c51d-11ea-918b-5194d600d068.png + :width: 300px +.. |Plasmasvg| image:: https://user-images.githubusercontent.com/8583900/94805331-226e1a80-03e4-11eb-8623-3e6db0aa1489.png + :width: 380px + +.. automodule:: paramak.parametric_components.tokamak_plasma + :members: + :show-inheritance: + +PlasmaFromPoints() +^^^^^^^^^^^^^^^^^^ + +|PlasmaFPstp| |PlasmaFPsvg| + +.. |PlasmaFPstp| image:: https://user-images.githubusercontent.com/8583900/87316638-f39b8300-c51d-11ea-918b-5194d600d068.png + :width: 300px +.. |PlasmaFPsvg| image:: https://user-images.githubusercontent.com/8583900/94805330-213ced80-03e4-11eb-80b4-b162f2f7a565.png + :width: 380px + +.. automodule:: paramak.parametric_components.tokamak_plasma_from_points + :members: + :show-inheritance: + +PlasmaBoundaries() +^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/8583900/97366104-a958ca80-189e-11eb-8bc6-9892b04ab053.png + :width: 300px + +.. automodule:: paramak.parametric_components.tokamak_plasma_plasmaboundaries + :members: + :show-inheritance: + +TFCoilCasing() +^^^^^^^^^^^^^^ + +|TFCoilCasingallstp| |TFCoilCasingaCutstp| |TFCoilCasingsvg| + +.. |TFCoilCasingallstp| image:: https://user-images.githubusercontent.com/8583900/98821523-94943f00-2427-11eb-8047-68f2762c56d7.png + :width: 200px +.. |TFCoilCasingaCutstp| image:: https://user-images.githubusercontent.com/8583900/98821532-96f69900-2427-11eb-99e1-e2461be67511.png + :width: 130px +.. |TFCoilCasingsvg| image:: https://user-images.githubusercontent.com/8583900/99081345-904c5b00-25ba-11eb-8a4f-956d4ad6bbc0.png + :width: 310px + +.. automodule:: paramak.parametric_components.tf_coil_casing + :members: + :show-inheritance: + + +ToroidalFieldCoilRectangle() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +|ToroidalFieldCoilRectangleallstp| |ToroidalFieldCoilRectanglesvg| |ToroidalFieldCoilRectangleastp| + +.. |ToroidalFieldCoilRectangleallstp| image:: https://user-images.githubusercontent.com/8583900/86822598-bcdbed80-c083-11ea-820e-f6c13d639170.png + :width: 200px +.. |ToroidalFieldCoilRectangleastp| image:: https://user-images.githubusercontent.com/8583900/94585086-6abbfa00-0277-11eb-91de-0b2548601587.png + :width: 130px +.. |ToroidalFieldCoilRectanglesvg| image:: https://user-images.githubusercontent.com/56687624/98582375-cd62d580-22ba-11eb-8002-ea7c731bad8a.png + :width: 310px + +.. automodule:: paramak.parametric_components.toroidal_field_coil_rectangle + :members: + :show-inheritance: + +ToroidalFieldCoilCoatHanger() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +|ToroidalFieldCoilCoatHangerallstp| |ToroidalFieldCoilCoatHangersvg| |ToroidalFieldCoilCoatHangerastp| + +.. |ToroidalFieldCoilCoatHangersvg| image:: https://user-images.githubusercontent.com/56687624/98582371-cb991200-22ba-11eb-8e15-86d273a8b819.png + :width: 300px +.. |ToroidalFieldCoilCoatHangerastp| image:: https://user-images.githubusercontent.com/8583900/98979392-3775b780-2513-11eb-9649-46839571f5dd.png + :width: 130px +.. |ToroidalFieldCoilCoatHangerallstp| image:: https://user-images.githubusercontent.com/8583900/87075236-f04f8100-c217-11ea-9ffa-4791b722b9e7.png + :width: 210px + +.. automodule:: paramak.parametric_components.toroidal_field_coil_coat_hanger + :members: + :show-inheritance: + +ToroidalFieldCoilPrincetonD() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +|ToroidalFieldCoilPrincetonDallstp| |ToroidalFieldCoilPrincetonDsvg| |ToroidalFieldCoilPrincetonDastp| + +.. |ToroidalFieldCoilPrincetonDallstp| image:: https://user-images.githubusercontent.com/56687624/92124475-bd7bd080-edf5-11ea-9c49-1db6422d77a0.png + :width: 250px +.. |ToroidalFieldCoilPrincetonDsvg| image:: https://user-images.githubusercontent.com/8583900/94529559-cd8aa280-0231-11eb-9919-48d3c642a5d7.png + :width: 220px +.. |ToroidalFieldCoilPrincetonDastp| image:: https://user-images.githubusercontent.com/8583900/94479853-4c012900-01cd-11eb-9b59-0fcd5f4dc531.png + :width: 170px + +.. automodule:: paramak.parametric_components.toroidal_field_coil_princeton_d + :members: + :show-inheritance: + +ToroidalFieldCoilTripleArc() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +|ToroidalFieldCoilTripleArcallstp| |ToroidalFieldCoilTripleArcstp| |ToroidalFieldCoilTripleArcsvg| + +.. |ToroidalFieldCoilTripleArcallstp| image:: https://user-images.githubusercontent.com/56687624/92124454-b654c280-edf5-11ea-96d2-c0957f37a733.png + :width: 240px +.. |ToroidalFieldCoilTripleArcstp| image:: https://user-images.githubusercontent.com/8583900/94835218-51e34e00-0409-11eb-9372-0272c43a4844.png + :width: 190px +.. |ToroidalFieldCoilTripleArcsvg| image:: https://user-images.githubusercontent.com/8583900/99848162-2eb75e00-2b71-11eb-80d2-6c695b56821d.png + :width: 320px + +.. automodule:: paramak.parametric_components.toroidal_field_coil_triple_arc + :members: + :show-inheritance: + +VacuumVessel() +^^^^^^^^^^^^^^ + +|VacuumVesselstp| |VacuumVesselsvgWP| |VacuumVesselsvg| + +.. |VacuumVesselstp| image:: https://user-images.githubusercontent.com/8583900/95792842-2d765400-0cdc-11eb-8a8a-e3a88e923bc0.png + :width: 150 +.. |VacuumVesselsvgWP| image:: https://user-images.githubusercontent.com/8583900/95792839-2c452700-0cdc-11eb-9313-edfd2bfad5dc.png + :width: 350 +.. |VacuumVesselsvg| image:: https://user-images.githubusercontent.com/8583900/95792893-4ed74000-0cdc-11eb-9d19-c66cdb3a5ca3.png + :width: 255 + +.. automodule:: paramak.parametric_components.vacuum_vessel + :members: + :show-inheritance: + +CoolantChannelRingStraight() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +|CoolantChannelRingStraightallstp| |CoolantChannelRingStraightsvg| |CoolantChannelRingStraightstp| + +.. |CoolantChannelRingStraightallstp| image:: https://user-images.githubusercontent.com/56687624/99049969-6467b000-258f-11eb-93b2-73e533b366c0.png + :width: 200 +.. |CoolantChannelRingStraightsvg| image:: https://user-images.githubusercontent.com/56687624/99048848-ff5f8a80-258d-11eb-9073-123185d7a4fb.png + :width: 230 +.. |CoolantChannelRingStraightstp| image:: https://user-images.githubusercontent.com/56687624/99049922-574ac100-258f-11eb-8cb9-a57cef2b8086.png + :width: 100 + +.. automodule:: paramak.parametric_components.coolant_channel_ring_straight + :members: + :show-inheritance: + +CoolantChannelRingCurved() +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +|CoolantChannelRingCurvedallstp| |CoolantChannelRingCurvedsvg| |CoolantChannelRingCurvedstp| + +.. |CoolantChannelRingCurvedallstp| image:: https://user-images.githubusercontent.com/56687624/99049933-5ade4800-258f-11eb-96c1-4506a8f646a9.png + :width: 200 +.. |CoolantChannelRingCurvedsvg| image:: https://user-images.githubusercontent.com/56687624/99048853-0090b780-258e-11eb-862e-763f7a0f7ec6.png + :width: 230 +.. |CoolantChannelRingCurvedstp| image:: https://user-images.githubusercontent.com/56687624/99049900-4f8b1c80-258f-11eb-81be-bc101e2168e2.png + :width: 100 + +.. automodule:: paramak.parametric_components.coolant_channel_ring_curved + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/paramak.parametric_neutronics.rst b/docs/source/paramak.parametric_neutronics.rst new file mode 100644 index 000000000..8b747e5e6 --- /dev/null +++ b/docs/source/paramak.parametric_neutronics.rst @@ -0,0 +1,38 @@ +Parametric Neutronics +===================== + +The Paramak supports automated neutronics model creation and subsequent +simulation. + +The neutronics models created are DAGMC models and are therefore compatible +with a suite of neutronics codes (MCNP, Fluka, Geant4, OpenMC). + +The automated simulations supported within the paramak are via OpenMC however one +could also carry out simulations in other neutronics codes using the dagmc.h5m +file created. Moab can be used to inspect the dagmc.h5 file and file the +material tag names. mbsize -ll dagmc.h5m | grep 'mat:' + +The creation of the dagmc.h5m file can be carried out via two routes. + +Option 1. Use of the `OCC_Faceter `_ +and the `PPP `_ + +Option 2. Use of `Trelis `_ by Coreform + +To create a model it is also necessary to define the plasma source parameters, +the materials used. + +Details of the plasma source used are available via the +`Git repository `_ + +Details of the Neutronics Material Maker are available from the +`documentation `_ +and the `source code repository `_ +. However openmc.Materials can also be used directly. + +NeutronicsModelFromReactor() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: paramak.parametric_neutronics.neutronics_model_from_reactor + :members: + :show-inheritance: diff --git a/docs/source/paramak.parametric_reactors.rst b/docs/source/paramak.parametric_reactors.rst new file mode 100644 index 000000000..a1cb56d39 --- /dev/null +++ b/docs/source/paramak.parametric_reactors.rst @@ -0,0 +1,128 @@ +Parametric Reactors +=================== + +These are the current reactor designs that can be created using the Paramak. + +.. image:: https://user-images.githubusercontent.com/8583900/99137324-fddfa200-2621-11eb-9063-f5f7f60ddd8d.png + :width: 713 + :align: center + +BallReactor() +^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/8583900/99136724-91af6f00-261e-11eb-9956-476b818a0ee3.png + :width: 400 + :align: center + +The above image is colored by components. The TF coils are blue, the PF coils +are red, PF coil cases are yellow, the center column shielding is dark green, +the blanket is light green, the divertor is orange, the firstwall is grey +and the rear wall of the blanket is teal. + +.. image:: https://user-images.githubusercontent.com/8583900/99298720-09a9af00-2842-11eb-816b-86492555f97d.png + :width: 450 + :align: center + +.. automodule:: paramak.parametric_reactors.ball_reactor + :members: + :show-inheritance: + +SegmentedBlanketBallReactor() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/8583900/99136727-94aa5f80-261e-11eb-965d-0ccceb2743fc.png + :width: 400 + :align: center + +The above image is colored by components. The TF coils are blue, the PF coils +are red, PF coil cases are yellow, the center column shielding is dark green, +the blanket is light green, the divertor is orange, the firstwall is grey +and the rear wall of the blanket is teal. + +Note that there is an odd number of blanket segments in this diagram so that +the blanket breeder zone and the first wall can be see in this 180 slice. + +.. image:: https://user-images.githubusercontent.com/8583900/99431100-1db4e580-2902-11eb-82ce-3f864d13524c.png + :width: 450 + :align: center + +Note the above image has the plasma purposefully hidden on the right hand side +so that the internal blanket structure can be seen. + +.. automodule:: paramak.parametric_reactors.segmented_blanket_ball_reactor + :members: + :show-inheritance: + +SingleNullBallReactor() +^^^^^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/8583900/99136728-983de680-261e-11eb-8398-51ae433f5546.png + :width: 400 + :align: center + +The above image is colored by components. The TF coils are blue, the PF coils +are red, PF coil cases are yellow, the center column shielding is dark green, +the blanket is light green, the divertor is orange, the firstwall is grey and +the rear wall of the blanket is teal. + +.. automodule:: paramak.parametric_reactors.single_null_ball_reactor + :members: + :show-inheritance: + +SubmersionTokamak() +^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/8583900/99136719-8e1be800-261e-11eb-907d-a9bafaebdbb8.png + :width: 400 + :align: center + +The above image is colored by components, the TF coils are blue, the PF coils +are red, PF coil cases are yellow, the center column shielding is dark green, the blanket is light green, the +divertor is orange, the firstwall is grey and the rear wall of the blanket is +teal and the support legs are black. + +.. image:: https://user-images.githubusercontent.com/8583900/99829028-aaa2ad80-2b53-11eb-9fd2-eba7521a0289.png + :width: 450 + :align: center + +.. automodule:: paramak.parametric_reactors.submersion_reactor + :members: + :show-inheritance: + +SingleNullSubmersionTokamak() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/8583900/99136731-9aa04080-261e-11eb-87a5-502708dfebcc.png + :width: 400 + :align: center + +The above image is colored by component. The TF coils are blue, the PF coils +are red, PF coil cases are yellow, the center column shielding is dark green, +the blanket is light green, the divertor is orange, the firstwall is grey, the +rear wall of the blanket is teal and the supports are black. + +.. automodule:: paramak.parametric_reactors.single_null_submersion_reactor + :members: + :show-inheritance: + +CenterColumnStudyReactor() +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/8583900/99136734-9e33c780-261e-11eb-837b-16a0bc59f8a7.png + :width: 400 + :align: center + +The above image is colored by component. The center column shielding is dark +green, the blanket is light green, the divertor is orange, the firstwall is +grey and the blanket is teal. + +Note this reactor is purposefully simple so that center column parameter +studies can be performed quickly. + +.. image:: https://user-images.githubusercontent.com/8583900/98946297-9e7f7600-24eb-11eb-92cd-1c3bd13ad49b.png + :width: 600 + :align: center + +.. automodule:: paramak.parametric_reactors.center_column_study_reactor + :members: + :show-inheritance: diff --git a/docs/source/paramak.parametric_shapes.rst b/docs/source/paramak.parametric_shapes.rst new file mode 100644 index 000000000..811d305cb --- /dev/null +++ b/docs/source/paramak.parametric_shapes.rst @@ -0,0 +1,148 @@ +Parametric Shapes +================= + + + +RotateStraightShape() +^^^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/8583900/86246786-767a2080-bba3-11ea-90e7-22d816690caa.png + :width: 250 + :height: 200 + :align: center + +.. automodule:: paramak.parametric_shapes.rotate_straight_shape + :members: + :show-inheritance: + +RotateSplineShape() +^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/8583900/86246785-7548f380-bba3-11ea-90b7-03249be41a00.png + :width: 250 + :height: 240 + :align: center + +.. automodule:: paramak.parametric_shapes.rotate_spline_shape + :members: + :show-inheritance: + +RotateMixedShape() +^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/8583900/86258771-17240c80-bbb3-11ea-990f-e87de26b1589.png + :width: 250 + :height: 230 + :align: center + +.. automodule:: paramak.parametric_shapes.rotate_mixed_shape + :members: + :show-inheritance: + +RotateCircleShape() +^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/56687624/98713427-868ae380-237f-11eb-87af-cf6b5fe032b2.png + :width: 250 + :height: 200 + :align: center + +.. automodule:: paramak.parametric_shapes.rotate_circle_shape + :members: + :show-inheritance: + +ExtrudeStraightShape() +^^^^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/8583900/86246776-724e0300-bba3-11ea-91c9-0fd239225206.png + :width: 200 + :height: 270 + :align: center + +.. automodule:: paramak.parametric_shapes.extruded_straight_shape + :members: + :show-inheritance: + +ExtrudeSplineShape() +^^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/56687624/98713431-87bc1080-237f-11eb-9075-01bca99b7018.png + :width: 200 + :height: 280 + :align: center + +.. automodule:: paramak.parametric_shapes.extruded_spline_shape + :members: + :show-inheritance: + +ExtrudeMixedShape() +^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/8583900/86261239-34a6a580-bbb6-11ea-812c-ac6fa6a8f0e2.png + :width: 200 + :height: 200 + :align: center + +.. automodule:: paramak.parametric_shapes.extruded_mixed_shape + :members: + :show-inheritance: + +ExtrudeCircleShape() +^^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/8583900/86246768-6feba900-bba3-11ea-81a8-0d77a843b943.png + :width: 250 + :height: 180 + :align: center + +.. automodule:: paramak.parametric_shapes.extruded_circle_shape + :members: + :show-inheritance: + +SweepStraightShape() +^^^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/56687624/98713447-8c80c480-237f-11eb-8615-c090e93138f6.png + :width: 300 + :height: 230 + :align: center + +.. automodule:: paramak.parametric_shapes.sweep_straight_shape + :members: + :show-inheritance: + +SweepSplineShape() +^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/56687624/98713443-8b4f9780-237f-11eb-83bb-38ca7f222073.png + :width: 300 + :height: 230 + :align: center + +.. automodule:: paramak.parametric_shapes.sweep_spline_shape + :members: + :show-inheritance: + +SweepMixedShape() +^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/56687624/98713440-8a1e6a80-237f-11eb-9eed-12b9d7731090.png + :width: 300 + :height: 230 + :align: center + +.. automodule:: paramak.parametric_shapes.sweep_mixed_shape + :members: + :show-inheritance: + +SweepCircleShape() +^^^^^^^^^^^^^^^^^^ + +.. image:: https://user-images.githubusercontent.com/56687624/98713436-88ed3d80-237f-11eb-99cd-27dcb4f313b1.png + :width: 300 + :height: 230 + :align: center + +.. automodule:: paramak.parametric_shapes.sweep_circle_shape + :members: + :show-inheritance: diff --git a/docs/source/tests.rst b/docs/source/tests.rst new file mode 100644 index 000000000..2c8197698 --- /dev/null +++ b/docs/source/tests.rst @@ -0,0 +1,31 @@ +Test Suite and automation +========================= + +A series of unit and integration tests are run automatically with every pull +request or merge to the Github repository. Running the tests locally is also +possible by running pytest from the paramak based directory. + +.. code-block:: bash + + pip install pytest + +.. code-block:: bash + + pytest tests + +The status of the tests is available on the CircleCI account +`CircleCI account. `_ + +The test suite can be explored on the +`Gihub source code repository. `_ + +In addition to automated tests we also have automated code style formatting +using `autopep8 and Github Actions. `_ + +Continuing the theme of automation we also have automated distribution updates. +The distribution is performed by `PyPI `_ and this is kept +upto date using Github Actions +`(upload python package) `_ +which trigger on every merge to the main branch. + +There are also plans for a continiously updated Dockerhub image in the pipeline. diff --git a/environment.yml b/environment.yml new file mode 100644 index 000000000..8d6d7990c --- /dev/null +++ b/environment.yml @@ -0,0 +1,19 @@ +name: docs-environment + +channels: + - conda-forge + - cadquery + +dependencies: + - pytest + - plotly + - scipy + - numpy + - matplotlib + - Pillow + - cadquery=2 + - openmc + - pip: + - neutronics-material-maker + - sympy + - plasmaboundaries \ No newline at end of file diff --git a/examples/example_neutronics_simulations/ball_reactor.py b/examples/example_neutronics_simulations/ball_reactor.py new file mode 100644 index 000000000..84395ec66 --- /dev/null +++ b/examples/example_neutronics_simulations/ball_reactor.py @@ -0,0 +1,75 @@ +"""This example makes a reactor geometry and a neutronics model. A homogenised +material made of enriched lithium lead and eurofer is being used as the blanket +material for this simulation in order to demonstrate the use of more complex +materials.""" + +import neutronics_material_maker as nmm +import paramak + + +def make_model_and_simulate(): + """Makes a neutronics Reactor model and simulates the TBR""" + + # makes the 3d geometry + my_reactor = paramak.BallReactor( + inner_bore_radial_thickness=1, + inboard_tf_leg_radial_thickness=30, + center_column_shield_radial_thickness=60, + divertor_radial_thickness=50, + inner_plasma_gap_radial_thickness=30, + plasma_radial_thickness=300, + outer_plasma_gap_radial_thickness=30, + firstwall_radial_thickness=3, + blanket_radial_thickness=100, + blanket_rear_wall_radial_thickness=3, + elongation=2.75, + triangularity=0.5, + number_of_tf_coils=16, + rotation_angle=360, + ) + + # makes a homogenised material for the blanket from lithium lead and + # eurofer + blanket_material = nmm.MultiMaterial( + fracs=[0.8, 0.2], + materials=[ + nmm.Material( + 'Pb842Li158', + enrichment=90, + temperature_in_K=500), + nmm.Material('eurofer') + ]) + + source = openmc.Source() + # sets the location of the source to x=0 y=0 z=0 + source.space = openmc.stats.Point((my_reactor.major_radius, 0, 0)) + # sets the direction to isotropic + source.angle = openmc.stats.Isotropic() + # sets the energy distribution to 100% 14MeV neutrons + source.energy = openmc.stats.Discrete([14e6], [1]) + + # makes the neutronics material + neutronics_model = paramak.NeutronicsModelFromReactor( + reactor=my_reactor, + source=source, + materials={ + 'inboard_tf_coils_mat': 'copper', + 'center_column_shield_mat': 'WC', + 'divertor_mat': 'eurofer', + 'firstwall_mat': 'eurofer', + 'blanket_mat': blanket_material, # use of homogenised material + 'blanket_rear_wall_mat': 'eurofer'}, + cell_tallies=['TBR'], + simulation_batches=5, + simulation_particles_per_batch=1e4, + ) + + # starts the neutronics simulation + neutronics_model.simulate(method='trelis') + + # prints the results to screen + print('TBR', neutronics_model.results['TBR']) + + +if __name__ == "__main__": + make_model_and_simulate() diff --git a/examples/example_neutronics_simulations/ball_reactor_minimal.py b/examples/example_neutronics_simulations/ball_reactor_minimal.py new file mode 100644 index 000000000..2f9a599e0 --- /dev/null +++ b/examples/example_neutronics_simulations/ball_reactor_minimal.py @@ -0,0 +1,58 @@ +"""This is a minimal example that obtains the TBR (Tritium Breeding Ratio) +for a parametric ball reactor""" + +import paramak + + +def make_model_and_simulate(): + """Makes a neutronics Reactor model and simulates the TBR""" + + # makes the 3d geometry from input parameters + my_reactor = paramak.BallReactor( + inner_bore_radial_thickness=50, + inboard_tf_leg_radial_thickness=200, + center_column_shield_radial_thickness=50, + divertor_radial_thickness=50, + inner_plasma_gap_radial_thickness=50, + plasma_radial_thickness=100, + outer_plasma_gap_radial_thickness=50, + firstwall_radial_thickness=1, + blanket_radial_thickness=100, + blanket_rear_wall_radial_thickness=10, + elongation=2, + triangularity=0.55, + number_of_tf_coils=16, + rotation_angle=360, + ) + + source = openmc.Source() + # sets the location of the source to x=0 y=0 z=0 + source.space = openmc.stats.Point((my_reactor.major_radius, 0, 0)) + # sets the direction to isotropic + source.angle = openmc.stats.Isotropic() + # sets the energy distribution to 100% 14MeV neutrons + source.energy = openmc.stats.Discrete([14e6], [1]) + + # makes the neutronics model from the geometry and material allocations + neutronics_model = paramak.NeutronicsModelFromReactor( + reactor=my_reactor, + source=source, + materials={ + 'inboard_tf_coils_mat': 'eurofer', + 'center_column_shield_mat': 'eurofer', + 'divertor_mat': 'eurofer', + 'firstwall_mat': 'eurofer', + 'blanket_rear_wall_mat': 'eurofer', + 'blanket_mat': 'Li4SiO4'}, + cell_tallies=['TBR', 'heating'], + simulation_batches=5, + simulation_particles_per_batch=1e4, + ) + + # simulate the neutronics model + neutronics_model.simulate(method='trelis') + print(neutronics_model.results) + + +if __name__ == "__main__": + make_model_and_simulate() diff --git a/examples/example_neutronics_simulations/center_column_study_reactor.py b/examples/example_neutronics_simulations/center_column_study_reactor.py new file mode 100644 index 000000000..0847f62c8 --- /dev/null +++ b/examples/example_neutronics_simulations/center_column_study_reactor.py @@ -0,0 +1,81 @@ +"""This example makes a reactor geometry and a neutronics model, the addition +of a Python for loop allow a parameter sweep of the neutronics results for +different geometries. The distance between the plasma and center column is +varried while simulating the impact on the heat depositied in the center +column.""" + +import matplotlib.pyplot as plt +import paramak + + +def make_model_and_simulate(): + """Makes a neutronics Reactor model and simulates the TBR""" + + total_heats_in_MW = [] + plasma_to_center_column_gaps = [] + + # this will take a few mins to perform 3 simulations at + for plasma_to_center_column_gap in [50, 100, 150]: + + # makes the 3d geometry + my_reactor = paramak.CenterColumnStudyReactor( + inner_bore_radial_thickness=20, + inboard_tf_leg_radial_thickness=50, + center_column_shield_radial_thickness_mid=50, + center_column_shield_radial_thickness_upper=100, + inboard_firstwall_radial_thickness=2, + divertor_radial_thickness=100, + inner_plasma_gap_radial_thickness=plasma_to_center_column_gap, + plasma_radial_thickness=200, + outer_plasma_gap_radial_thickness=90, + elongation=2.75, + triangularity=0.5, + plasma_gap_vertical_thickness=40, + center_column_arc_vertical_thickness=520, + rotation_angle=360 + ) + + source = openmc.Source() + # sets the location of the source to x=0 y=0 z=0 + source.space = openmc.stats.Point((my_reactor.major_radius, 0, 0)) + # sets the direction to isotropic + source.angle = openmc.stats.Isotropic() + # sets the energy distribution to 100% 14MeV neutrons + source.energy = openmc.stats.Discrete([14e6], [1]) + + # makes the neutronics model and assigns basic materials to each + # component + neutronics_model = paramak.NeutronicsModelFromReactor( + reactor=my_reactor, + source=source, + materials={ + 'DT_plasma': 'DT_plasma', + 'inboard_tf_coils_mat': 'eurofer', + 'center_column_shield_mat': 'eurofer', + 'divertor_mat': 'eurofer', + 'firstwall_mat': 'eurofer', + 'blanket_mat': 'Li4SiO4'}, + cell_tallies=['heating'], + simulation_batches=5, + simulation_particles_per_batch=1e4, + ) + + # starts the neutronics simulation + neutronics_model.simulate(method='trelis') + + # converts the results to mega watts + total_heat_in_MW = neutronics_model.results['firstwall_mat_heating']['Watts']['result'] / 1e6 + + # adds the results and inputs to a list + total_heats_in_MW.append(total_heat_in_MW) + plasma_to_center_column_gaps.append(plasma_to_center_column_gap) + + # plots the results + plt.scatter(plasma_to_center_column_gaps, total_heats_in_MW) + plt.xlabel('plasma_to_center_column_gap (cm)') + plt.ylabel('Heat on the inboard (MW)') + plt.show() + + +if __name__ == "__main__": + make_model_and_simulate() diff --git a/examples/example_neutronics_simulations/center_column_study_reactor_minimal.py b/examples/example_neutronics_simulations/center_column_study_reactor_minimal.py new file mode 100644 index 000000000..ee8b97028 --- /dev/null +++ b/examples/example_neutronics_simulations/center_column_study_reactor_minimal.py @@ -0,0 +1,59 @@ +"""This is a minimal example that obtains the center column heating for a +parametric reactor.""" + +import paramak + + +def make_model_and_simulate(): + """Makes a neutronics Reactor model and simulates the heat deposition""" + + # makes the 3d geometry + my_reactor = paramak.CenterColumnStudyReactor( + inner_bore_radial_thickness=20, + inboard_tf_leg_radial_thickness=50, + center_column_shield_radial_thickness_mid=50, + center_column_shield_radial_thickness_upper=100, + inboard_firstwall_radial_thickness=20, + divertor_radial_thickness=100, + inner_plasma_gap_radial_thickness=80, + plasma_radial_thickness=200, + outer_plasma_gap_radial_thickness=90, + elongation=2.75, + triangularity=0.5, + plasma_gap_vertical_thickness=40, + center_column_arc_vertical_thickness=520, + rotation_angle=360 + ) + + source = openmc.Source() + # sets the location of the source to x=0 y=0 z=0 + source.space = openmc.stats.Point((my_reactor.major_radius, 0, 0)) + # sets the direction to isotropic + source.angle = openmc.stats.Isotropic() + # sets the energy distribution to 100% 14MeV neutrons + source.energy = openmc.stats.Discrete([14e6], [1]) + + # creates a neutronics model from the geometry and assigned materials + neutronics_model = paramak.NeutronicsModelFromReactor( + reactor=my_reactor, + source=source, + materials={ + 'inboard_tf_coils_mat': 'eurofer', + 'center_column_shield_mat': 'eurofer', + 'divertor_mat': 'eurofer', + 'firstwall_mat': 'eurofer', + 'blanket_mat': 'Li4SiO4'}, + cell_tallies=['heating'], + simulation_batches=5, + simulation_particles_per_batch=1e4, + ) + + # starts the neutronics simulation + neutronics_model.simulate(method='trelis') + + # prints the results + print(neutronics_model.results) + + +if __name__ == "__main__": + make_model_and_simulate() diff --git a/examples/example_neutronics_simulations/make_faceteted_neutronics_model.py b/examples/example_neutronics_simulations/make_faceteted_neutronics_model.py new file mode 100644 index 000000000..c587a46f0 --- /dev/null +++ b/examples/example_neutronics_simulations/make_faceteted_neutronics_model.py @@ -0,0 +1,166 @@ +#!/usr/env/python3 +import json +import os + +# This script automatically produces DAGMC compatable geometry. A manifest +# file is required that specfies a the stp filenames and the materials names to +# assign. The name of the manifest file is manifest.json by default but can be +# specified using aprepro arguments. Other optional aprepro arguments are +# faceting_tolerance and merge_tolerance which default to 1e-1 and 1e-4 by +# default + +# To using this script with Trelis it can be run in batch mode +# trelis -batch -nographics make_faceteted_neutronics_model.py + +# With the Trelis GUI +# trelis make_faceteted_neutronics_model.py + +# With additional arguments to overwrite the defaults +# trelis -batch -nographics make_faceteted_neutronics_model.py "faceting_tolerance='1e-4'" "merge_tolerance='1e-4'" + +# An example manifest file would contain a list of dictionaries with entry having +# stp_filename and material keywords. Here is an example manifest file with just +# two entries. + +# [ +# { +# "material": "m1", +# "stp_filename": "inboard_tf_coils.stp", +# }, +# { +# "material": "m2", +# "stp_filename": "center_column_shield.stp", +# } +# ] + + +def find_number_of_volumes_in_each_step_file(input_locations, basefolder): + body_ids = "" + volumes_in_each_step_file = [] + # all_groups=cubit.parse_cubit_list("group","all") + # starting_group_id = len(all_groups) + for entry in input_locations: + # starting_group_id = starting_group_id +1 + current_vols = cubit.parse_cubit_list("volume", "all") + print(os.path.join(basefolder, entry["stp_filename"])) + if entry["stp_filename"].endswith(".sat"): + import_type = "acis" + if entry["stp_filename"].endswith( + ".stp") or entry["stp_filename"].endswith(".step"): + import_type = "step" + short_file_name = os.path.split(entry["stp_filename"])[-1] + # print('short_file_name',short_file_name) + # cubit.cmd('import '+import_type+' "' + entry['stp_filename'] + '" separate_bodies no_surfaces no_curves no_vertices group "'+str(short_file_name)+'"') + cubit.cmd( + "import " + + import_type + + ' "' + + os.path.join(basefolder, entry["stp_filename"]) + + '" separate_bodies no_surfaces no_curves no_vertices ' + ) + all_vols = cubit.parse_cubit_list("volume", "all") + new_vols = set(current_vols).symmetric_difference(set(all_vols)) + new_vols = map(str, new_vols) + print("new_vols", new_vols, type(new_vols)) + current_bodies = cubit.parse_cubit_list("body", "all") + print("current_bodies", current_bodies) + # volumes_in_group = cubit.cmd('volume in group '+str(starting_group_id)) + # print('volumes_in_group',volumes_in_group,type(volumes_in_group)) + if len(new_vols) > 1: + cubit.cmd( + "unite vol " + + " ".join(new_vols) + + " with vol " + + " ".join(new_vols)) + all_vols = cubit.parse_cubit_list("volume", "all") + new_vols_after_unite = set( + current_vols).symmetric_difference(set(all_vols)) + new_vols_after_unite = map(str, new_vols_after_unite) + # cubit.cmd('group '+str(starting_group_id)+' copy rotate 45 about z repeat 7') + entry["volumes"] = new_vols_after_unite + cubit.cmd( + 'group "' + + short_file_name + + '" add volume ' + + " ".join( + entry["volumes"])) + # cubit.cmd('volume in group '+str(starting_group_id)+' copy rotate 45 about z repeat 7') + cubit.cmd("separate body all") + return input_locations + + +def byteify(input): + if isinstance(input, dict): + return {byteify(key): byteify(value) + for key, value in input.iteritems()} + elif isinstance(input, list): + return [byteify(element) for element in input] + elif isinstance(input, unicode): + return input.encode("utf-8") + else: + return input + + +def tag_geometry_with_mats(geometry_details): + for entry in geometry_details: + cubit.cmd( + 'group "mat:' + + entry["material"] + + '" add volume ' + + " ".join(entry["volumes"]) + ) + + +def imprint_and_merge_geometry(): + cubit.cmd("imprint body all") + print('using merge_tolerance of ', merge_tolerance) + cubit.cmd("merge tolerance " + merge_tolerance) # optional as there is a default + cubit.cmd("merge vol all group_results") + cubit.cmd("graphics tol angle 3") + + +def save_output_files(): + """This saves the output files""" + cubit.cmd("set attribute on") + # use a faceting_tolerance 1.0e-4 or smaller for accurate simulations + print('using faceting_tolerance of ', faceting_tolerance) + cubit.cmd('export dagmc "dagmc_not_watertight.h5m" faceting_tolerance '+ faceting_tolerance) + # os.system('mbconvert -1 dagmc_not_watertight.h5m dagmc_not_watertight_edges.h5m') + with open("geometry_details.json", "w") as outfile: + json.dump(geometry_details, outfile, indent=4) + +aprepro_vars = cubit.get_aprepro_vars() + +print("Found the following aprepro variables:") +print(aprepro_vars) +for var_name in aprepro_vars: + val = cubit.get_aprepro_value_as_string(var_name) + print("{0} = {1}".format(var_name, val)) + +if "faceting_tolerance" in aprepro_vars: + faceting_tolerance = str(cubit.get_aprepro_value_as_string("faceting_tolerance")) +else: + faceting_tolerance = str(1.0e-1) + +if "merge_tolerance" in aprepro_vars: + merge_tolerance = str(cubit.get_aprepro_value_as_string("merge_tolerance")) +else: + merge_tolerance = str(1e-4) + +if "manifest" in aprepro_vars: + manifest_filename = str(cubit.get_aprepro_value_as_string("manifest")) +else: + manifest_filename = "manifest.json" + +with open(manifest_filename) as f: + geometry_details = byteify(json.load(f)) + + +geometry_details = find_number_of_volumes_in_each_step_file( \ + geometry_details, os.path.abspath(".")) + +tag_geometry_with_mats(geometry_details) + +imprint_and_merge_geometry() + +save_output_files() diff --git a/examples/example_neutronics_simulations/make_unstructured_mesh.py b/examples/example_neutronics_simulations/make_unstructured_mesh.py new file mode 100644 index 000000000..2f72faa63 --- /dev/null +++ b/examples/example_neutronics_simulations/make_unstructured_mesh.py @@ -0,0 +1,139 @@ +#!/usr/env/python3 +import json +import os + + +def byteify(input): + if isinstance(input, dict): + return {byteify(key): byteify(value) + for key, value in input.iteritems()} + elif isinstance(input, list): + return [byteify(element) for element in input] + elif isinstance(input, unicode): + return input.encode("utf-8") + else: + return input + + +def save_tet_details_to_json_file( + geometry_details, + filename="mesh_details.json"): + for entry in geometry_details: + material = entry["material"] + tets_in_volumes = cubit.parse_cubit_list( + "tet", " in volume " + " ".join(entry["volumes"]) + ) + print("material ", material, " has ", len(tets_in_volumes), " tets") + entry["tet_ids"] = tets_in_volumes + with open(filename, "w") as outfile: + json.dump(geometry_details, outfile, indent=4) + + +def find_number_of_volumes_in_each_step_file(input_locations, basefolder): + body_ids = "" + volumes_in_each_step_file = [] + # all_groups=cubit.parse_cubit_list("group","all") + # starting_group_id = len(all_groups) + for entry in input_locations: + # starting_group_id = starting_group_id +1 + current_vols = cubit.parse_cubit_list("volume", "all") + print(os.path.join(basefolder, entry["stp_filename"])) + if entry["stp_filename"].endswith(".sat"): + import_type = "acis" + if entry["stp_filename"].endswith( + ".stp") or entry["stp_filename"].endswith(".step"): + import_type = "step" + short_file_name = os.path.split(entry["stp_filename"])[-1] + # print('short_file_name',short_file_name) + # cubit.cmd('import '+import_type+' "' + entry['stp_filename'] + '" separate_bodies no_surfaces no_curves no_vertices group "'+str(short_file_name)+'"') + cubit.cmd( + "import " + + import_type + + ' "' + + os.path.join(basefolder, entry["stp_filename"]) + + '" separate_bodies no_surfaces no_curves no_vertices ' + ) + all_vols = cubit.parse_cubit_list("volume", "all") + new_vols = set(current_vols).symmetric_difference(set(all_vols)) + new_vols = map(str, new_vols) + print("new_vols", new_vols, type(new_vols)) + current_bodies = cubit.parse_cubit_list("body", "all") + print("current_bodies", current_bodies) + # volumes_in_group = cubit.cmd('volume in group '+str(starting_group_id)) + # print('volumes_in_group',volumes_in_group,type(volumes_in_group)) + if len(new_vols) > 1: + cubit.cmd( + "unite vol " + + " ".join(new_vols) + + " with vol " + + " ".join(new_vols)) + all_vols = cubit.parse_cubit_list("volume", "all") + new_vols_after_unite = set( + current_vols).symmetric_difference(set(all_vols)) + new_vols_after_unite = map(str, new_vols_after_unite) + # cubit.cmd('group '+str(starting_group_id)+' copy rotate 45 about z repeat 7') + entry["volumes"] = new_vols_after_unite + cubit.cmd( + 'group "' + + short_file_name + + '" add volume ' + + " ".join( + entry["volumes"])) + # cubit.cmd('volume in group '+str(starting_group_id)+' copy rotate 45 about z repeat 7') + cubit.cmd("separate body all") + return input_locations + + +def imprint_and_merge_geometry(tolerance="1e-4"): + cubit.cmd("imprint body all") + cubit.cmd("merge tolerance " + tolerance) # optional as there is a default + cubit.cmd("merge vol all group_results") + cubit.cmd("graphics tol angle 3") + + +cubit.cmd("reset") + +cubit.cmd("set attribute on") + +with open("manifest.json") as json_file: + data = byteify(json.load(json_file)) + +input_locations = [] +for entry in data: + if "tet_mesh" in entry.keys(): + input_locations.append(entry) +geometry_details = find_number_of_volumes_in_each_step_file( + input_locations, str(os.path.abspath(".")) +) + +imprint_and_merge_geometry() + +current_vols = cubit.parse_cubit_list("volume", "all") + +cubit.cmd("Trimesher volume gradation 1.3") + +cubit.cmd("volume all size auto factor 5") +print(geometry_details) +for entry in geometry_details: + for volume in entry["volumes"]: + cubit.cmd( + "volume " + str(volume) + " size auto factor 6" + ) # this number is the size of the mesh 1 is small 10 is large + cubit.cmd( + "volume all scheme tetmesh proximity layers off geometric sizing on") + if "size" in entry["mesh"]: + cubit.cmd("volume " + str(volume) + " " + + entry["tet_mesh"]) # ' size 0.5' + else: + cubit.cmd("volume " + str(volume)) + cubit.cmd("mesh volume " + str(volume)) + + +cubit.cmd('export mesh "tet_mesh.exo" overwrite') +# cubit.cmd('export abaqus "tet_mesh.inp" overwrite') # asci format, not goood for large meshes +# cubit.cmd('save as "tet_mesh.cub" overwrite') # mbconvert code is older +# than the exo equivilent + +print("unstrutured mesh saved as tet_mesh.exo") + +save_tet_details_to_json_file(geometry_details) diff --git a/examples/example_neutronics_simulations/segmented_blanket_ball_reactor.py b/examples/example_neutronics_simulations/segmented_blanket_ball_reactor.py new file mode 100644 index 000000000..0f4154337 --- /dev/null +++ b/examples/example_neutronics_simulations/segmented_blanket_ball_reactor.py @@ -0,0 +1,247 @@ +"""This example makes a reactor geometry, neutronics model and performs a TBR +simulation. A selection of materials are used from refrenced sources to +complete the neutronics model.""" + +import neutronics_material_maker as nmm +import paramak + + +def make_model_and_simulate(): + """Makes a neutronics Reactor model and simulates the TBR with specified materials""" + + # based on + # http://www.euro-fusionscipub.org/wp-content/uploads/WPBBCP16_15535_submitted.pdf + firstwall_radial_thickness = 3.0 + firstwall_armour_material = "tungsten" + firstwall_coolant_material = "He" + firstwall_structural_material = "eurofer" + firstwall_armour_fraction = 0.106305 + firstwall_coolant_fraction = 0.333507 + firstwall_coolant_temperature_C = 400 + firstwall_coolant_pressure_Pa = 8e6 + firstwall_structural_fraction = 0.560188 + + firstwall_material = nmm.MultiMaterial( + material_tag="firstwall_mat", + materials=[ + nmm.Material( + material_name=firstwall_coolant_material, + temperature_in_C=firstwall_coolant_temperature_C, + pressure_in_Pa=firstwall_coolant_pressure_Pa, + ), + nmm.Material(material_name=firstwall_structural_material), + nmm.Material(material_name=firstwall_armour_material), + ], + fracs=[ + firstwall_coolant_fraction, + firstwall_structural_fraction, + firstwall_armour_fraction, + ], + percent_type="vo" + ) + + # based on + # https://www.sciencedirect.com/science/article/pii/S2352179118300437 + blanket_rear_wall_coolant_material = "H2O" + blanket_rear_wall_structural_material = "eurofer" + blanket_rear_wall_coolant_fraction = 0.3 + blanket_rear_wall_structural_fraction = 0.7 + blanket_rear_wall_coolant_temperature_C = 200 + blanket_rear_wall_coolant_pressure_Pa = 1e6 + + blanket_rear_wall_material = nmm.MultiMaterial( + material_tag="blanket_rear_wall_mat", + materials=[ + nmm.Material( + material_name=blanket_rear_wall_coolant_material, + temperature_in_C=blanket_rear_wall_coolant_temperature_C, + pressure_in_Pa=blanket_rear_wall_coolant_pressure_Pa, + ), + nmm.Material(material_name=blanket_rear_wall_structural_material), + ], + fracs=[ + blanket_rear_wall_coolant_fraction, + blanket_rear_wall_structural_fraction, + ], + percent_type="vo" + ) + + # based on + # https://www.sciencedirect.com/science/article/pii/S2352179118300437 + blanket_lithium6_enrichment_percent = 60 + blanket_breeder_material = "Li4SiO4" + blanket_coolant_material = "He" + blanket_multiplier_material = "Be" + blanket_structural_material = "eurofer" + blanket_breeder_fraction = 0.15 + blanket_coolant_fraction = 0.05 + blanket_multiplier_fraction = 0.6 + blanket_structural_fraction = 0.2 + blanket_breeder_packing_fraction = 0.64 + blanket_multiplier_packing_fraction = 0.64 + blanket_coolant_temperature_C = 500 + blanket_coolant_pressure_Pa = 1e6 + blanket_breeder_temperature_C = 600 + blanket_breeder_pressure_Pa = 8e6 + + blanket_material = nmm.MultiMaterial( + material_tag="blanket_mat", + materials=[ + nmm.Material( + material_name=blanket_coolant_material, + temperature_in_C=blanket_coolant_temperature_C, + pressure_in_Pa=blanket_coolant_pressure_Pa, + ), + nmm.Material(material_name=blanket_structural_material), + nmm.Material( + material_name=blanket_multiplier_material, + packing_fraction=blanket_multiplier_packing_fraction, + ), + nmm.Material( + material_name=blanket_breeder_material, + enrichment=blanket_lithium6_enrichment_percent, + packing_fraction=blanket_breeder_packing_fraction, + temperature_in_C=blanket_breeder_temperature_C, + pressure_in_Pa=blanket_breeder_pressure_Pa, + ), + ], + fracs=[ + blanket_coolant_fraction, + blanket_structural_fraction, + blanket_multiplier_fraction, + blanket_breeder_fraction, + ], + percent_type="vo" + ) + + # based on + # https://www.sciencedirect.com/science/article/pii/S2352179118300437 + divertor_coolant_fraction = 0.57195798876 + divertor_structural_fraction = 0.42804201123 + divertor_coolant_material = "H2O" + divertor_structural_material = "tungsten" + divertor_coolant_temperature_C = 150 + divertor_coolant_pressure_Pa = 5e6 + + divertor_material = nmm.MultiMaterial( + material_tag="divertor_mat", + materials=[ + nmm.Material( + material_name=divertor_coolant_material, + temperature_in_C=divertor_coolant_temperature_C, + pressure_in_Pa=divertor_coolant_pressure_Pa, + ), + nmm.Material(material_name=divertor_structural_material), + ], + fracs=[divertor_coolant_fraction, divertor_structural_fraction], + percent_type="vo" + ) + + # based on + # https://pdfs.semanticscholar.org/95fa/4dae7d82af89adf711b97e75a241051c7129.pdf + center_column_shield_coolant_fraction = 0.13 + center_column_shield_structural_fraction = 0.57 + center_column_shield_coolant_material = "H2O" + center_column_shield_structural_material = "tungsten" + center_column_shield_coolant_temperature_C = 150 + center_column_shield_coolant_pressure_Pa = 5e6 + + center_column_shield_material = nmm.MultiMaterial( + material_tag="center_column_shield_mat", + materials=[ + nmm.Material( + material_name=center_column_shield_coolant_material, + temperature_in_C=center_column_shield_coolant_temperature_C, + pressure_in_Pa=center_column_shield_coolant_pressure_Pa, + ), + nmm.Material( + material_name=center_column_shield_structural_material), + ], + fracs=[ + center_column_shield_coolant_fraction, + center_column_shield_structural_fraction, + ], + percent_type="vo") + + # based on + # https://pdfs.semanticscholar.org/95fa/4dae7d82af89adf711b97e75a241051c7129.pdf + inboard_tf_coils_conductor_fraction = 0.57 + inboard_tf_coils_coolant_fraction = 0.05 + inboard_tf_coils_structure_fraction = 0.38 + inboard_tf_coils_conductor_material = "copper" + inboard_tf_coils_coolant_material = "He" + inboard_tf_coils_structure_material = "SS_316L_N_IG" + inboard_tf_coils_coolant_temperature_C = 30 + inboard_tf_coils_coolant_pressure_Pa = 8e6 + + inboard_tf_coils_material = nmm.MultiMaterial( + material_tag="inboard_tf_coils_mat", + materials=[ + nmm.Material( + material_name=inboard_tf_coils_coolant_material, + temperature_in_C=inboard_tf_coils_coolant_temperature_C, + pressure_in_Pa=inboard_tf_coils_coolant_pressure_Pa, + ), + nmm.Material(material_name=inboard_tf_coils_conductor_material), + nmm.Material(material_name=inboard_tf_coils_structure_material), + ], + fracs=[ + inboard_tf_coils_coolant_fraction, + inboard_tf_coils_conductor_fraction, + inboard_tf_coils_structure_fraction, + ], + percent_type="vo" + ) + + # makes the 3d geometry + my_reactor = paramak.BallReactor( + inner_bore_radial_thickness=1, + inboard_tf_leg_radial_thickness=30, + center_column_shield_radial_thickness=60, + divertor_radial_thickness=50, + inner_plasma_gap_radial_thickness=30, + plasma_radial_thickness=300, + outer_plasma_gap_radial_thickness=30, + firstwall_radial_thickness=firstwall_radial_thickness, + # http://www.euro-fusionscipub.org/wp-content/uploads/WPBBCP16_15535_submitted.pdf + blanket_radial_thickness=100, + blanket_rear_wall_radial_thickness=3, + elongation=2.75, + triangularity=0.5, + number_of_tf_coils=16, + rotation_angle=360, + ) + + source = openmc.Source() + # sets the location of the source to x=0 y=0 z=0 + source.space = openmc.stats.Point((my_reactor.major_radius, 0, 0)) + # sets the direction to isotropic + source.angle = openmc.stats.Isotropic() + # sets the energy distribution to 100% 14MeV neutrons + source.energy = openmc.stats.Discrete([14e6], [1]) + + # makes the neutronics material + neutronics_model = paramak.NeutronicsModelFromReactor( + reactor=my_reactor, + source=source, + materials={ + 'inboard_tf_coils_mat': inboard_tf_coils_material, + 'center_column_shield_mat': center_column_shield_material, + 'divertor_mat': divertor_material, + 'firstwall_mat': firstwall_material, + 'blanket_mat': blanket_material, + 'blanket_rear_wall_mat': blanket_rear_wall_material}, + cell_tallies=['TBR'], + simulation_batches=5, + simulation_particles_per_batch=1e4, + ) + + # starts the neutronics simulation + neutronics_model.simulate(method='trelis') + + # prints the simulation results to screen + print('TBR', neutronics_model.results['TBR']) + + +if __name__ == "__main__": + make_model_and_simulate() diff --git a/examples/example_neutronics_simulations/submersion_reactor.py b/examples/example_neutronics_simulations/submersion_reactor.py new file mode 100644 index 000000000..29062a34a --- /dev/null +++ b/examples/example_neutronics_simulations/submersion_reactor.py @@ -0,0 +1,79 @@ +"""This is a example that obtains the tritium breeding ratio (TBR) +for a parametric submersion reactor and specified the faceting and merge +tolerance when creating the dagmc model""" + +import matplotlib.pyplot as plt +import neutronics_material_maker as nmm +import paramak + + +def make_model_and_simulate(temperature): + """Makes a neutronics Reactor model and simulates the flux""" + + # makes the 3d geometry from input parameters + my_reactor = paramak.SubmersionTokamak( + inner_bore_radial_thickness=30, + inboard_tf_leg_radial_thickness=30, + center_column_shield_radial_thickness=30, + divertor_radial_thickness=80, + inner_plasma_gap_radial_thickness=50, + plasma_radial_thickness=200, + outer_plasma_gap_radial_thickness=50, + firstwall_radial_thickness=30, + blanket_rear_wall_radial_thickness=30, + rotation_angle=180, + support_radial_thickness=50, + inboard_blanket_radial_thickness=30, + outboard_blanket_radial_thickness=30, + elongation=2.75, + triangularity=0.5, + ) + + # this can just be set as a string as temperature is needed for this + # material + flibe = nmm.Material('FLiBe', temperature_in_C=temperature) + + source = openmc.Source() + # sets the location of the source to x=0 y=0 z=0 + source.space = openmc.stats.Point((my_reactor.major_radius, 0, 0)) + # sets the direction to isotropic + source.angle = openmc.stats.Isotropic() + # sets the energy distribution to 100% 14MeV neutrons + source.energy = openmc.stats.Discrete([14e6], [1]) + + # makes the neutronics model from the geometry and material allocations + neutronics_model = paramak.NeutronicsModelFromReactor( + reactor=my_reactor, + source=source, + materials={ + 'inboard_tf_coils_mat': 'eurofer', + 'center_column_shield_mat': 'eurofer', + 'divertor_mat': 'eurofer', + 'firstwall_mat': 'eurofer', + 'blanket_rear_wall_mat': 'eurofer', + 'blanket_mat': flibe, + 'supports_mat': 'eurofer'}, + cell_tallies=['TBR'], + simulation_batches=5, + simulation_particles_per_batch=1e4, + faceting_tolerance=1e-4, + merge_tolerance=1e-4 + ) + + # simulate the neutronics model + neutronics_model.simulate(method='trelis') + return neutronics_model.results['TBR'] + + +if __name__ == "__main__": + tbr_values = [] + temperature_values = [32, 100, 200, 300, 400, 500] + for temperature in temperature_values: + tbr = make_model_and_simulate(temperature) + tbr_values.append(tbr) + + # plots the results + plt.scatter(temperature_values, tbr_values) + plt.xlabel('FLiBe Temperature (degrees C)') + plt.ylabel('TBR') + plt.show() diff --git a/examples/example_neutronics_simulations/submersion_reactor_minimal.py b/examples/example_neutronics_simulations/submersion_reactor_minimal.py new file mode 100644 index 000000000..f7fa7a417 --- /dev/null +++ b/examples/example_neutronics_simulations/submersion_reactor_minimal.py @@ -0,0 +1,65 @@ +"""This is a minimal example that obtains the particle flux in each component +for a parametric submersion reactor""" + +import neutronics_material_maker as nmm +import paramak + + +def make_model_and_simulate(): + """Makes a neutronics Reactor model and simulates the flux""" + + # makes the 3d geometry from input parameters + my_reactor = paramak.SubmersionTokamak( + inner_bore_radial_thickness=30, + inboard_tf_leg_radial_thickness=30, + center_column_shield_radial_thickness=30, + divertor_radial_thickness=80, + inner_plasma_gap_radial_thickness=50, + plasma_radial_thickness=200, + outer_plasma_gap_radial_thickness=50, + firstwall_radial_thickness=30, + blanket_rear_wall_radial_thickness=30, + rotation_angle=180, + support_radial_thickness=50, + inboard_blanket_radial_thickness=30, + outboard_blanket_radial_thickness=30, + elongation=2.75, + triangularity=0.5, + ) + + # this can just be set as a string as temperature is needed for this + # material + flibe = nmm.Material('FLiBe', temperature_in_C=500) + + source = openmc.Source() + # sets the location of the source to x=0 y=0 z=0 + source.space = openmc.stats.Point((my_reactor.major_radius, 0, 0)) + # sets the direction to isotropic + source.angle = openmc.stats.Isotropic() + # sets the energy distribution to 100% 14MeV neutrons + source.energy = openmc.stats.Discrete([14e6], [1]) + + # makes the neutronics model from the geometry and material allocations + neutronics_model = paramak.NeutronicsModelFromReactor( + reactor=my_reactor, + source=source, + materials={ + 'inboard_tf_coils_mat': 'eurofer', + 'center_column_shield_mat': 'eurofer', + 'divertor_mat': 'eurofer', + 'firstwall_mat': 'eurofer', + 'blanket_rear_wall_mat': 'eurofer', + 'blanket_mat': flibe, + 'supports_mat': 'eurofer'}, + cell_tallies=['flux'], + simulation_batches=5, + simulation_particles_per_batch=1e4, + ) + + # simulate the neutronics model + neutronics_model.simulate(method='trelis') + print(neutronics_model.results) + + +if __name__ == "__main__": + make_model_and_simulate() diff --git a/examples/example_parametric_components/make_all_parametric_components.py b/examples/example_parametric_components/make_all_parametric_components.py new file mode 100644 index 000000000..5348f4fec --- /dev/null +++ b/examples/example_parametric_components/make_all_parametric_components.py @@ -0,0 +1,392 @@ +""" +This python script demonstrates the creation of all parametric shapes available +in the paramak tool +""" + +import paramak + + +def main(): + + rot_angle = 180 + all_components = [] + + plasma = paramak.Plasma( + # default parameters + rotation_angle=rot_angle, + stp_filename="plasma_shape.stp", + ) + all_components.append(plasma) + + component = paramak.BlanketFP( + plasma=plasma, + thickness=100, + stop_angle=90, + start_angle=-90, + offset_from_plasma=30, + rotation_angle=rot_angle, + stp_filename="blanket_constant_thickness_outboard_plasma.stp", + ) + all_components.append(component) + + component = paramak.BlanketCutterStar( + height=2000, + width=2000, + distance=100) + all_components.append(component) + + component = paramak.BlanketFP( + plasma=plasma, + thickness=100, + stop_angle=90, + start_angle=250, + offset_from_plasma=30, + rotation_angle=rot_angle, + stp_filename="blanket_constant_thickness_inboard_plasma.stp", + ) + all_components.append(component) + + component = paramak.BlanketFP( + plasma=plasma, + thickness=100, + stop_angle=250, + start_angle=-90, + offset_from_plasma=30, + rotation_angle=rot_angle, + stp_filename="blanket_constant_thickness_plasma.stp", + ) + all_components.append(component) + + CenterColumnShieldCylinder = paramak.CenterColumnShieldCylinder( + inner_radius=80, + outer_radius=100, + height=300, + rotation_angle=rot_angle, + stp_filename="center_column_shield_cylinder.stp", + ) + all_components.append(CenterColumnShieldCylinder) + + component = paramak.InboardFirstwallFCCS( + central_column_shield=CenterColumnShieldCylinder, + thickness=50, + rotation_angle=rot_angle, + stp_filename="firstwall_from_center_column_shield_cylinder.stp", + ) + all_components.append(component) + + CenterColumnShieldHyperbola = paramak.CenterColumnShieldHyperbola( + inner_radius=50, + mid_radius=75, + outer_radius=100, + height=300, + rotation_angle=rot_angle, + stp_filename="center_column_shield_hyperbola.stp", + ) + all_components.append(CenterColumnShieldHyperbola) + + component = paramak.InboardFirstwallFCCS( + central_column_shield=CenterColumnShieldHyperbola, + thickness=50, + rotation_angle=rot_angle, + stp_filename="firstwall_from_center_column_shield_hyperbola.stp", + ) + all_components.append(component) + + CenterColumnShieldCircular = paramak.CenterColumnShieldCircular( + inner_radius=50, + mid_radius=75, + outer_radius=100, + height=300, + rotation_angle=rot_angle, + stp_filename="center_column_shield_circular.stp", + ) + all_components.append(CenterColumnShieldCircular) + + component = paramak.InboardFirstwallFCCS( + central_column_shield=CenterColumnShieldCircular, + thickness=50, + rotation_angle=rot_angle, + stp_filename="firstwall_from_center_column_shield_circular.stp", + ) + all_components.append(component) + + CenterColumnShieldFlatTopHyperbola = paramak.CenterColumnShieldFlatTopHyperbola( + inner_radius=50, + mid_radius=75, + outer_radius=100, + arc_height=220, + height=300, + rotation_angle=rot_angle, + stp_filename="center_column_shield_flat_top_hyperbola.stp", + ) + all_components.append(CenterColumnShieldFlatTopHyperbola) + + component = paramak.InboardFirstwallFCCS( + central_column_shield=CenterColumnShieldFlatTopHyperbola, + thickness=50, + rotation_angle=rot_angle, + stp_filename="firstwall_from_center_column_shield_flat_top_hyperbola.stp", + ) + all_components.append(component) + + CenterColumnShieldFlatTopCircular = paramak.CenterColumnShieldFlatTopCircular( + inner_radius=50, + mid_radius=75, + outer_radius=100, + arc_height=220, + height=300, + rotation_angle=rot_angle, + stp_filename="center_column_shield_flat_top_Circular.stp", + ) + all_components.append(CenterColumnShieldFlatTopCircular) + + component = paramak.InboardFirstwallFCCS( + central_column_shield=CenterColumnShieldFlatTopCircular, + thickness=50, + rotation_angle=rot_angle, + stp_filename="firstwall_from_center_column_shield_flat_top_Circular.stp", + ) + all_components.append(component) + + CenterColumnShieldPlasmaHyperbola = paramak.CenterColumnShieldPlasmaHyperbola( + inner_radius=150, + mid_offset=50, + edge_offset=40, + height=800, + rotation_angle=rot_angle, + stp_filename="center_column_shield_plasma_hyperbola.stp", + ) + all_components.append(CenterColumnShieldPlasmaHyperbola) + + component = paramak.InboardFirstwallFCCS( + central_column_shield=CenterColumnShieldPlasmaHyperbola, + thickness=50, + rotation_angle=rot_angle, + stp_filename="firstwall_from_center_column_shield_plasma_hyperbola.stp", + ) + all_components.append(component) + + component = paramak.InnerTfCoilsCircular( + inner_radius=25, + outer_radius=100, + number_of_coils=10, + gap_size=5, + height=300, + stp_filename="inner_tf_coils_circular.stp", + ) + all_components.append(component) + + component = paramak.InnerTfCoilsFlat( + inner_radius=25, + outer_radius=100, + number_of_coils=10, + gap_size=5, + height=300, + stp_filename="inner_tf_coils_flat.stp", + ) + all_components.append(component) + + # this makes 4 pf coil cases + pf_coil_set = paramak.PoloidalFieldCoilCaseSet( + heights=[10, 10, 20, 20], + widths=[10, 10, 20, 40], + casing_thicknesses=[5, 5, 10, 10], + center_points=[(100, 100), (100, 150), (50, 200), (50, 50)], + rotation_angle=rot_angle, + stp_filename="pf_coil_case_set.stp" + ) + all_components.append(pf_coil_set) + + # this makes 4 pf coils + pf_coil_set = paramak.PoloidalFieldCoilSet( + heights=[10, 10, 20, 20], + widths=[10, 10, 20, 40], + center_points=[(100, 100), (100, 150), (50, 200), (50, 50)], + rotation_angle=rot_angle, + stp_filename="pf_coil_set.stp" + ) + all_components.append(pf_coil_set) + + # this makes 4 pf coil cases for the 4 pf coils made above + component = paramak.PoloidalFieldCoilCaseSetFC( + pf_coils=pf_coil_set, + casing_thicknesses=[5, 5, 10, 10], + rotation_angle=rot_angle, + stp_filename="pf_coil_cases_set.stp" + ) + all_components.append(component) + + # this makes 1 pf coils + pf_coil = paramak.PoloidalFieldCoil( + center_point=(100, 100), + height=20, + width=20, + rotation_angle=rot_angle, + stp_filename="poloidal_field_coil.stp" + ) + all_components.append(pf_coil) + + # this makes one PF coil case for the provided pf coil + component = paramak.PoloidalFieldCoilCaseSetFC( + pf_coils=[pf_coil], + casing_thicknesses=[10], + rotation_angle=rot_angle, + stp_filename="pf_coil_cases_set_fc.stp") + all_components.append(component) + + component = paramak.PoloidalFieldCoilCaseFC( + pf_coil=pf_coil, + casing_thickness=10, + rotation_angle=rot_angle, + stp_filename="poloidal_field_coil_case_fc.stp", + ) + all_components.append(component) + + component = paramak.PoloidalFieldCoilCase( + center_point=(100, 100), + coil_height=20, + coil_width=20, + casing_thickness=10, + rotation_angle=rot_angle, + stp_filename="poloidal_field_coil_case.stp", + ) + all_components.append(component) + + component = paramak.BlanketConstantThicknessArcV( + inner_lower_point=(300, -200), + inner_mid_point=(500, 0), + inner_upper_point=(300, 200), + thickness=100, + rotation_angle=rot_angle, + stp_filename="blanket_arc_v.stp", + ) + all_components.append(component) + + component = paramak.BlanketConstantThicknessArcH( + inner_lower_point=(300, -200), + inner_mid_point=(400, 0), + inner_upper_point=(300, 200), + thickness=100, + rotation_angle=rot_angle, + stp_filename="blanket_arc_h.stp", + ) + all_components.append(component) + + component = paramak.ToroidalFieldCoilRectangle( + horizontal_start_point=(100, 700), + vertical_mid_point=(800, 0), + thickness=150, + distance=60, + stp_filename="tf_coil_rectangle.stp", + number_of_coils=1, + ) + all_components.append(component) + + component = paramak.ToroidalFieldCoilCoatHanger( + horizontal_start_point=(200, 500), + horizontal_length=400, + vertical_mid_point=(700, 50), + vertical_length=500, + thickness=50, + distance=50, + stp_filename="toroidal_field_coil_coat_hanger.stp", + number_of_coils=1, + ) + all_components.append(component) + + component = paramak.ToroidalFieldCoilTripleArc( + R1=80, + h=200, + radii=(70, 100), + coverages=(60, 60), + thickness=30, + distance=30, + number_of_coils=1, + stp_filename="toroidal_field_coil_triple_arc.stp" + ) + all_components.append(component) + + component = paramak.ToroidalFieldCoilPrincetonD( + R1=80, + R2=300, + thickness=30, + distance=30, + number_of_coils=1, + stp_filename="toroidal_field_coil_princeton_d.stp" + ) + all_components.append(component) + + component = paramak.ITERtypeDivertor( + # default parameters + rotation_angle=rot_angle, + stp_filename="ITER_type_divertor.stp", + ) + all_components.append(component) + + component = paramak.PortCutterRotated( + center_point=(450, 0), + polar_coverage_angle=20, + rotation_angle=10, + polar_placement_angle=45, + azimuth_placement_angle=0 + ) + all_components.append(component) + + component = paramak.PortCutterRectangular( + distance=3, + z_pos=0, + height=0.2, + width=0.4, + fillet_radius=0.02, + azimuth_placement_angle=[0, 45, 90, 180] + ) + all_components.append(component) + + component = paramak.PortCutterCircular( + distance=3, + z_pos=0.25, + radius=0.1, + # azimuth_placement_angle=[0, 45, 90, 180], # TODO: fix issue #548 + azimuth_placement_angle=[0, 45, 90], + ) + all_components.append(component) + + component = paramak.VacuumVessel( + height=2, inner_radius=1, thickness=0.2, rotation_angle=270 + ) + all_components.append(component) + + component = paramak.CoolantChannelRingStraight( + height=200, + channel_radius=10, + ring_radius=70, + number_of_coolant_channels=8, + workplane="XY", + rotation_axis="Z", + stp_filename="coolant_channel_ring_straight.stp", + ) + all_components.append(component) + + component = paramak.CoolantChannelRingCurved( + height=200, + channel_radius=10, + ring_radius=70, + mid_offset=-20, + number_of_coolant_channels=8, + workplane="XY", + path_workplane="XZ", + stp_filename="coolant_channel_ring_curved.stp", + force_cross_section=True + ) + all_components.append(component) + + return all_components + + +if __name__ == "__main__": + all_components = main() + filenames = [] + for components in all_components: + components.export_stp() + filenames.append(components.stp_filename) + print(filenames) diff --git a/examples/example_parametric_components/make_all_parametric_components_images_for_docs.py b/examples/example_parametric_components/make_all_parametric_components_images_for_docs.py new file mode 100644 index 000000000..33f57e809 --- /dev/null +++ b/examples/example_parametric_components/make_all_parametric_components_images_for_docs.py @@ -0,0 +1,20 @@ +""" +This python script demonstrates the creation of all parametric shapes available +in the paramak tool +""" + +from make_all_parametric_components import main + + +def export_images(): + + all_componets = main() + + for componet in all_componets: + componet.workplane = "XY" + componet.export_svg(componet.stp_filename[:-3] + "svg") + # os.system('conver') + + +if __name__ == "__main__": + export_images() diff --git a/examples/example_parametric_components/make_demo_style_blankets.py b/examples/example_parametric_components/make_demo_style_blankets.py new file mode 100644 index 000000000..ebad2f705 --- /dev/null +++ b/examples/example_parametric_components/make_demo_style_blankets.py @@ -0,0 +1,93 @@ + +""" +This script makes a blanket and then segments it in a similar +manner to the EU DEMO segmentation for remote maintenance. +""" + +import math + +import numpy as np +import paramak + + +def main(number_of_sections=8, gap_size=15, central_block_width=200): + + number_of_segments = 8 + gap_size = 15. + central_block_width = 200 + + offset = (360 / number_of_segments) / 2 + + # a plasma shape is made and used by the BlanketFP, which builds around + # the plasma + plasma = paramak.Plasma( + elongation=1.59, + triangularity=0.33, + major_radius=910, + minor_radius=290) + plasma.solid + + # this makes a cutter shape that is used to make the blanket bananna + # segment that has parallel sides + parallel_outboard_gaps_outer = paramak.BlanketCutterParallels( + thickness=gap_size, azimuth_placement_angle=np.linspace( + 0, 360, number_of_segments, endpoint=False), + gap_size=central_block_width) + + # this makes a gap that seperates the inboard and outboard blanket + inboard_to_outboard_gaps = paramak.ExtrudeStraightShape( + points=[(plasma.high_point[0] - (0.5 * gap_size), plasma.high_point[1]), + (plasma.high_point[0] - (0.5 * gap_size), plasma.high_point[1] + 1000), + (plasma.high_point[0] + (0.5 * gap_size), plasma.high_point[1] + 1000), + (plasma.high_point[0] + (0.5 * gap_size), plasma.high_point[1]), + ], + distance=math.tan(math.radians(360 / (2 * number_of_segments))) * plasma.high_point[0] * 2, + azimuth_placement_angle=np.linspace(0, 360, number_of_segments, endpoint=False) + ) + + # this makes the regular gaps (non parallel) gaps on the outboard blanket + outboard_gaps = paramak.BlanketCutterStar( + distance=gap_size, + azimuth_placement_angle=np.linspace( + 0 + offset, + 360 + offset, + number_of_segments, + endpoint=False) + ) + + # makes the outboard blanket with cuts for all the segmentation + outboard_blanket = paramak.BlanketFP( + plasma=plasma, + thickness=100, + stop_angle=90, + start_angle=-60, + offset_from_plasma=30, + rotation_angle=360, + cut=[ + outboard_gaps, + parallel_outboard_gaps_outer, + inboard_to_outboard_gaps]) + + # this makes the regular gaps on the outboard blanket + inboard_gaps = paramak.BlanketCutterStar( + distance=gap_size, azimuth_placement_angle=np.linspace( + 0, 360, number_of_segments * 2, endpoint=False)) + + # makes the inboard blanket with cuts for all the segmentation + inboard_blanket = paramak.BlanketFP( + plasma=plasma, + thickness=100, + stop_angle=90, + start_angle=260, + offset_from_plasma=30, + rotation_angle=360, + cut=[inboard_gaps, inboard_to_outboard_gaps], + union=outboard_blanket + ) + + # saves the blanket as an stp file + inboard_blanket.export_stp('blanket.stp') + + +if __name__ == "__main__": + main() diff --git a/examples/example_parametric_components/make_firstwall_for_neutron_wall_loading.py b/examples/example_parametric_components/make_firstwall_for_neutron_wall_loading.py new file mode 100644 index 000000000..841a95edf --- /dev/null +++ b/examples/example_parametric_components/make_firstwall_for_neutron_wall_loading.py @@ -0,0 +1,37 @@ + +""" +For some neutronics tallies such as neutron wall loading it is necessary to +segment the geometry so that individual neutronics tallies can be recorded +for each face. This can be done using the PoloidalSegments(). With this +segmented geometry it is then easier to find neutron wall loading as a function +of poloidal angle. +""" + +import paramak + + +def main(): + + # makes the firstwall + firstwall = paramak.BlanketFP(minor_radius=150, + major_radius=450, + triangularity=0.55, + elongation=2.0, + thickness=2, + start_angle=270, + stop_angle=-90, + rotation_angle=10) + + # segments the firstwall poloidally into 40 equal angle segments + segmented_firstwall = paramak.PoloidalSegments( + shape_to_segment=firstwall, + center_point=(450, 0), # this is the middle of the plasma + number_of_segments=40, + ) + + # saves the segmented firstwall as an stp file + segmented_firstwall.export_stp('segmented_firstwall.stp') + + +if __name__ == "__main__": + main() diff --git a/examples/example_parametric_components/make_magnet_set.py b/examples/example_parametric_components/make_magnet_set.py new file mode 100644 index 000000000..3bf050054 --- /dev/null +++ b/examples/example_parametric_components/make_magnet_set.py @@ -0,0 +1,77 @@ + +""" +This script makes a set of toroidal and poloidal field coils. Including PF +coils, PF coil cases, TF coils, TF coil cases and inboard TF coil supports. +""" + +import paramak + + +def main(): + + number_of_toroidal_field_coils = 8 + angle_offset = (360 / number_of_toroidal_field_coils) / 2. + tf_coil_thickness = 50 + tf_coil_distance = 130 + + tf_coil_casing_distance = tf_coil_distance + 40 + tf_coil_casing_thickness = 20 + + inner_tf_case = paramak.InnerTfCoilsFlat( + height=1800, + inner_radius=330, + outer_radius=430, + number_of_coils=number_of_toroidal_field_coils, + gap_size=5, + rotation_angle=180, + azimuth_start_angle=angle_offset + ) + + tf_coils = paramak.ToroidalFieldCoilPrincetonD( + R1=400, + R2=1500, # height + thickness=tf_coil_thickness, + distance=tf_coil_distance, + number_of_coils=number_of_toroidal_field_coils, + rotation_angle=180, + ) + + tf_coil_casing = paramak.TFCoilCasing( + magnet=tf_coils, + distance=tf_coil_casing_distance, + inner_offset=tf_coil_casing_thickness, + outer_offset=tf_coil_casing_thickness, + vertical_section_offset=tf_coil_casing_thickness, + # rotation_angle=180, # producing occational errors with this arg + ) + + pf_coils = paramak.PoloidalFieldCoilSet( + heights=[100, 120, 80, 80, 120, 180], + widths=[100, 120, 80, 80, 120, 180], + center_points=[ + (530, 1030), + (1370, 790), + (1740, 250), + (1750, -250), + (1360, -780), + (680, -1050) + ], + rotation_angle=180 + ) + + pf_coils_casing = paramak.PoloidalFieldCoilCaseSetFC( + pf_coils=pf_coils, + casing_thicknesses=[10] * 6, + rotation_angle=180 + ) + + pf_coils.export_stp('pf_coils.stp') + pf_coils_casing.export_stp('pf_coils_case.stp') + + tf_coils.export_stp('tf_coil.stp') + tf_coil_casing.export_stp('tf_coil_casing.stp') + inner_tf_case.export_stp('inner_tf_case.stp') + + +if __name__ == "__main__": + main() diff --git a/examples/example_parametric_components/make_plasmas.py b/examples/example_parametric_components/make_plasmas.py new file mode 100644 index 000000000..b2c80fe83 --- /dev/null +++ b/examples/example_parametric_components/make_plasmas.py @@ -0,0 +1,254 @@ +""" +This python script demonstrates the creation of plasmas +""" + +import paramak +import plotly.graph_objects as go + + +def plot_plasma(plasma, name=""): + """Extracts points that make up the plasma and creates a Plotly trace""" + + if name.endswith("plasmaboundaries"): + major_radius = plasma.major_radius + low_point = plasma.low_point + high_point = plasma.high_point + inner_equatorial_point = plasma.inner_equatorial_point + outer_equatorial_point = plasma.outer_equatorial_point + x_points = [row[0] for row in plasma.points] + y_points = [row[1] for row in plasma.points] + + else: + major_radius = plasma.major_radius * -1 + low_point = (plasma.low_point[0] * -1, plasma.low_point[1]) + high_point = (plasma.high_point[0] * -1, plasma.high_point[1]) + inner_equatorial_point = ( + plasma.inner_equatorial_point[0] * -1, + plasma.inner_equatorial_point[1]) + outer_equatorial_point = ( + plasma.outer_equatorial_point[0] * -1, + plasma.outer_equatorial_point[1]) + x_points = [row[0] * -1 for row in plasma.points] + y_points = [row[1] for row in plasma.points] + + traces = [] + + color_list = [i * 255 for i in plasma.color] + color = "rgb(" + str(color_list).strip("[]") + ")" + + traces.append( + go.Scatter( + x=[major_radius], + y=[0], + mode="markers", + name="major_radius", + marker={"color": color}, + ) + ) + + traces.append( + go.Scatter( + x=x_points, + y=y_points, + mode="markers", + name="points", + marker={"color": color}, + ) + ) + + traces.append( + go.Scatter( + x=[low_point[0]], + y=[low_point[1]], + mode="markers", + name="low_point", + marker={"color": color}, + ) + ) + + traces.append( + go.Scatter( + x=[high_point[0]], + y=[high_point[1]], + mode="markers", + name="high_point", + marker={"color": color}, + ) + ) + + traces.append( + go.Scatter( + x=[inner_equatorial_point[0]], + y=[inner_equatorial_point[1]], + mode="markers", + name="inner_equatorial_point", + marker={"color": color}, + ) + ) + + traces.append( + go.Scatter( + x=[outer_equatorial_point[0]], + y=[outer_equatorial_point[1]], + mode="markers", + name="outer_equatorial_point", + marker={"color": color}, + ) + ) + + return traces + + +def make_plasma( + major_radius, + minor_radius, + triangularity, + elongation, + name, + color): + """Creates a plasma object from argument inputs""" + + plasma = paramak.Plasma( + major_radius=major_radius, + minor_radius=minor_radius, + triangularity=triangularity, + elongation=elongation, + ) + plasma.name = "plasma" + plasma.stp_filename = name + ".stp" + plasma.single_null = True + plasma.color = color + plasma.rotation_angle = 180 + plasma.find_points() + plasma.export_2d_image(name + ".png") + plasma.export_html(name + ".html") + plasma.export_stp(name + ".stp") + plasma.create_solid() + + return plasma + + +def make_plasma_plasmaboundaries( + A, + major_radius, + minor_radius, + triangularity, + elongation, + name, + color, + config="single-null", +): + """Creates a plasma object from argument inputs""" + + plasma = paramak.PlasmaBoundaries( + A=A, + major_radius=major_radius, + minor_radius=minor_radius, + triangularity=triangularity, + elongation=elongation, + configuration=config, + rotation_angle=180, + color=color + ) + plasma.name = "plasma" + plasma.export_2d_image(name + ".png") + plasma.export_html(name + ".html") + plasma.export_stp(name + ".stp") + plasma.create_solid() + + return plasma + + +def main(): + + ITER_plasma = make_plasma( + name="ITER_plasma", + major_radius=620, + minor_radius=210, + triangularity=0.33, + elongation=1.85, + color=(0.1, 0.5, 0.7) + ) + + ITER_plasma_plasmaboundaries = make_plasma_plasmaboundaries( + name="ITER_plasma_plasmaboundaries", + A=-0.155, + major_radius=620, + minor_radius=210, + triangularity=0.33, + elongation=1.85, + color=(1, 0.5, 0) + ) + + EU_DEMO_plasma = make_plasma( + name="EU_DEMO_plasma", + major_radius=910, + minor_radius=290, + triangularity=0.33, + elongation=1.59, + color=(0.9, 0.1, 0.1) + ) + + ST_plasma = make_plasma( + name="ST_plasma", + major_radius=170, + minor_radius=129, + triangularity=0.55, + elongation=2.3, + color=(0.2, 0.6, 0.2) + ) + + AST_plasma = make_plasma( + name="AST_plasma", + major_radius=170, + minor_radius=129, + triangularity=-0.55, + elongation=2.3, + color=(0, 0, 0) + ) + + NSTX_double_null_plasma_plasmaboundaries = make_plasma_plasmaboundaries( + name="NSTX_double_null_plasma_plasmaboundaries", + A=0, + major_radius=850, + minor_radius=680, + triangularity=0.35, + elongation=2, + color=(1, 1, 0), + config="double-null" + ) + + NSTX_single_null_plasma_plasmaboundaries = make_plasma_plasmaboundaries( + name="NSTX_single_null_plasma_plasmaboundaries", + A=-0.05, + major_radius=850, + minor_radius=680, + triangularity=0.35, + elongation=2, + color=(0.6, 0.3, 0.6), + config="single-null" + ) + + fig = go.Figure() + fig.add_traces(plot_plasma(plasma=ITER_plasma, name="ITER_plasma")) + fig.add_traces( + plot_plasma( + plasma=ITER_plasma_plasmaboundaries, + name="ITER_plasma_plasmaboundaries")) + fig.add_traces(plot_plasma(plasma=EU_DEMO_plasma, name="EU_DEMO_plasma")) + fig.add_traces(plot_plasma(plasma=ST_plasma, name="ST_plasma")) + fig.add_traces(plot_plasma(plasma=AST_plasma, name="AST_plasma")) + fig.add_traces( + plot_plasma( + plasma=NSTX_double_null_plasma_plasmaboundaries, + name="NSTX_double_null_plasma_plasmaboundaries")) + fig.add_traces( + plot_plasma( + plasma=NSTX_single_null_plasma_plasmaboundaries, + name="NSTX_single_null_plasma_plasmaboundaries")) + fig.show() + fig.write_html("all_plasma_and_points.html") + + +if __name__ == "__main__": + main() diff --git a/examples/example_parametric_components/make_vacuum_vessel_with_ports.py b/examples/example_parametric_components/make_vacuum_vessel_with_ports.py new file mode 100644 index 000000000..9085eb41a --- /dev/null +++ b/examples/example_parametric_components/make_vacuum_vessel_with_ports.py @@ -0,0 +1,57 @@ + +""" +This python script demonstrates the creation of a vacuum vessel with shaped +different ports cut out. +""" + +import numpy as np +import paramak + + +def main(): + + number_of_ports_in_360_degrees = 12 + angles_for_ports = np.linspace(0, 360, number_of_ports_in_360_degrees) + + # makes the upper row of ports + rotated_ports = paramak.PortCutterRotated( + center_point=(0, 0), + polar_coverage_angle=10, + rotation_angle=10, + polar_placement_angle=25, + azimuth_placement_angle=angles_for_ports + ) + + # makes the middle row of ports + circular_ports = paramak.PortCutterCircular( + distance=5, + z_pos=0, + radius=0.2, + azimuth_placement_angle=angles_for_ports + ) + + # makes the lower row of ports + rectangular_ports = paramak.PortCutterRectangular( + distance=5, + z_pos=-1, + height=0.3, + width=0.4, + fillet_radius=0.08, + azimuth_placement_angle=angles_for_ports + ) + + # creates the hollow cylinder vacuum vessel and cuts away the ports + vacuum_vessel = paramak.VacuumVessel( + height=4, + inner_radius=2, + thickness=0.2, + cut=[rotated_ports, rectangular_ports, circular_ports] + ) + + # eports images and 3D CAD + vacuum_vessel.export_svg('vacuum_vessel_with_ports.svg') + vacuum_vessel.export_stp('vacuum_vessel_with_ports.stp') + + +if __name__ == "__main__": + main() diff --git a/examples/example_parametric_reactors/ball_reactor.py b/examples/example_parametric_reactors/ball_reactor.py new file mode 100644 index 000000000..00354ae91 --- /dev/null +++ b/examples/example_parametric_reactors/ball_reactor.py @@ -0,0 +1,51 @@ +""" +This example creates a ball reactor using the BallReactor parametric reactor. +By default the script saves stp, stl, html and svg files. +""" + +from pathlib import Path + +import paramak + + +def make_ball_reactor(outputs=['stp', 'neutronics', 'svg', 'stl', 'html'], + output_folder='BallReactor'): + + my_reactor = paramak.BallReactor( + inner_bore_radial_thickness=10, + inboard_tf_leg_radial_thickness=30, + center_column_shield_radial_thickness=60, + divertor_radial_thickness=150, + inner_plasma_gap_radial_thickness=30, + plasma_radial_thickness=300, + outer_plasma_gap_radial_thickness=30, + firstwall_radial_thickness=30, + blanket_radial_thickness=50, + blanket_rear_wall_radial_thickness=30, + elongation=2, + triangularity=0.55, + number_of_tf_coils=16, + rotation_angle=180, + pf_coil_radial_thicknesses=[50, 50, 50, 50], + pf_coil_vertical_thicknesses=[50, 50, 50, 50], + pf_coil_to_rear_blanket_radial_gap=50, + pf_coil_to_tf_coil_radial_gap=50, + outboard_tf_coil_radial_thickness=100, + outboard_tf_coil_poloidal_thickness=50 + ) + + if 'stp' in outputs: + my_reactor.export_stp(output_folder=output_folder) + if 'neutronics' in outputs: + my_reactor.export_neutronics_description( + Path(output_folder) / 'manifest.json') + if 'svg' in outputs: + my_reactor.export_svg(Path(output_folder) / 'reactor.svg') + if 'stl' in outputs: + my_reactor.export_stl(output_folder=output_folder) + if 'html' in outputs: + my_reactor.export_html(Path(output_folder) / 'reactor.html') + + +if __name__ == "__main__": + make_ball_reactor(['stp', 'neutronics', 'svg', 'stl', 'html']) diff --git a/examples/example_parametric_reactors/ball_reactor_single_null.py b/examples/example_parametric_reactors/ball_reactor_single_null.py new file mode 100644 index 000000000..6b8a49c0d --- /dev/null +++ b/examples/example_parametric_reactors/ball_reactor_single_null.py @@ -0,0 +1,52 @@ +""" +This example creates a single null ball reactor using the SingleNullBallReactor +parametric reactor. By default the script saves stp, stl, html and svg files. +""" + +from pathlib import Path + +import paramak + + +def make_ball_reactor_sn(outputs=['stp', 'neutronics', 'svg', 'stl', 'html'], + output_folder='BallReactor_sn'): + + my_reactor = paramak.SingleNullBallReactor( + inner_bore_radial_thickness=50, + inboard_tf_leg_radial_thickness=50, + center_column_shield_radial_thickness=50, + divertor_radial_thickness=90, + inner_plasma_gap_radial_thickness=50, + plasma_radial_thickness=200, + outer_plasma_gap_radial_thickness=50, + firstwall_radial_thickness=50, + blanket_radial_thickness=100, + blanket_rear_wall_radial_thickness=50, + elongation=2, + triangularity=0.55, + number_of_tf_coils=16, + rotation_angle=180, + pf_coil_radial_thicknesses=[50, 50, 50, 50], + pf_coil_vertical_thicknesses=[50, 50, 50, 50], + pf_coil_to_rear_blanket_radial_gap=50, + pf_coil_to_tf_coil_radial_gap=50, + outboard_tf_coil_radial_thickness=100, + outboard_tf_coil_poloidal_thickness=50, + divertor_position="lower" + ) + + if 'stp' in outputs: + my_reactor.export_stp(output_folder=output_folder) + if 'neutronics' in outputs: + my_reactor.export_neutronics_description( + Path(output_folder) / 'manifest.json') + if 'svg' in outputs: + my_reactor.export_svg(Path(output_folder) / 'reactor.svg') + if 'stl' in outputs: + my_reactor.export_stl(output_folder=output_folder) + if 'html' in outputs: + my_reactor.export_html(Path(output_folder) / 'reactor.html') + + +if __name__ == "__main__": + make_ball_reactor_sn(['stp', 'neutronics', 'svg', 'stl', 'html']) diff --git a/examples/example_parametric_reactors/center_column_study_reactor.py b/examples/example_parametric_reactors/center_column_study_reactor.py new file mode 100644 index 000000000..79a010c20 --- /dev/null +++ b/examples/example_parametric_reactors/center_column_study_reactor.py @@ -0,0 +1,60 @@ +""" +This example creates a center column study reactor using a parametric reactor. +Adds some TF coils to the reactor. By default the script saves stp, stl, +html and svg files. +""" + +import paramak + + +def make_center_column_study_reactor( + outputs=[ + 'stp', + 'neutronics', + 'svg', + 'stl', + 'html']): + + my_reactor = paramak.CenterColumnStudyReactor( + inner_bore_radial_thickness=20, + inboard_tf_leg_radial_thickness=50, + center_column_shield_radial_thickness_mid=50, + center_column_shield_radial_thickness_upper=100, + inboard_firstwall_radial_thickness=20, + divertor_radial_thickness=100, + inner_plasma_gap_radial_thickness=80, + plasma_radial_thickness=200, + outer_plasma_gap_radial_thickness=90, + elongation=2.3, + triangularity=0.45, + plasma_gap_vertical_thickness=40, + center_column_arc_vertical_thickness=520, + rotation_angle=180) + + # adding in some TF coils + tf_magnet = paramak.ToroidalFieldCoilPrincetonD( + R1=20 + 50, + R2=20 + 50 + 50 + 80 + 200 + 90 + 100 + 20, + thickness=50, + distance=50, + number_of_coils=12, + rotation_angle=180 + ) + + my_reactor.shapes_and_components.append(tf_magnet) + + if 'stp' in outputs: + my_reactor.export_stp(output_folder='CenterColumnStudyReactor') + if 'neutronics' in outputs: + my_reactor.export_neutronics_description( + 'CenterColumnStudyReactor/manifest.json') + if 'svg' in outputs: + my_reactor.export_svg('CenterColumnStudyReactor/reactor.svg') + if 'stl' in outputs: + my_reactor.export_stl(output_folder='CenterColumnStudyReactor') + if 'html' in outputs: + my_reactor.export_html('CenterColumnStudyReactor/reactor.html') + + +if __name__ == "__main__": + make_center_column_study_reactor() diff --git a/examples/example_parametric_reactors/htc_reactor.py b/examples/example_parametric_reactors/htc_reactor.py new file mode 100644 index 000000000..4ada138e0 --- /dev/null +++ b/examples/example_parametric_reactors/htc_reactor.py @@ -0,0 +1,264 @@ + +"""Example reactor based on the SPARC design as published in Figure 4 of +Overview of the SPARC tokamak. Journal of Plasma Physics, 86(5), 865860502. +doi:10.1017/S0022377820001257. Coordinates extracted from the figure are +not exact and therefore this model does not perfectly represent the reactor.""" + +import paramak + + +def main(rotation_angle=180): + + inboard_pf_coils = paramak.PoloidalFieldCoilSet( + center_points=[ + (53.5, -169.58), + (53.5, -118.43), + (53.5, -46.54), + (53.5, 46.54), + (53.5, 118.43), + (53.5, 169.58), + ], + heights=[41.5, 40.5, 82.95, 82.95, 40.5, 41.5, ], + widths=[27.7, 27.7, 27.7, 27.7, 27.7, 27.7], + rotation_angle=rotation_angle, + stp_filename='inboard_pf_coils.stp' + ) + + outboard_pf_coils = paramak.PoloidalFieldCoilSet( + center_points=[ + (86, 230), + (86, -230), + (164, 241), + (164, -241), + (263, 222), + (263, -222), + (373, 131), + (373, -131), + ], + widths=[32, 32, 50, 50, 43, 43, 48, 48, ], + heights=[40, 40, 30, 30, 28, 28, 37, 37, ], + rotation_angle=rotation_angle, + stp_filename='outboard_pf_coils.stp' + ) + + div_coils = paramak.PoloidalFieldCoilSet( + center_points=[ + (207, 144), + (207, 125), + (207, -144), + (207, -125), + + ], + widths=[15, 15, 15, 15], + heights=[15, 15, 15, 15], + rotation_angle=rotation_angle, + stp_filename='div_coils.stp' + ) + + vs_coils = paramak.PoloidalFieldCoilSet( + center_points=[ + (240, 70), + (240, -70), + ], + widths=[10, 10], + heights=[10, 10], + rotation_angle=rotation_angle, + stp_filename='vs_coils.stp' + ) + + EFCCu_coils_1 = paramak.RotateStraightShape( + points=[ + (235.56581986143186, -127.64976958525347), + (240.1847575057737, -121.19815668202767), + (246.65127020785218, -125.80645161290323), + (242.0323325635104, -132.25806451612902), + ], + rotation_angle=rotation_angle, + stp_filename='EFCCu_coils_1.stp' + ) + EFCCu_coils_2 = paramak.RotateStraightShape( + points=[ + (262.3556581986143, -90.78341013824888), + (266.97459584295615, -84.33179723502303), + (273.44110854503464, -88.94009216589859), + (268.82217090069287, -94.47004608294935), + ], + rotation_angle=rotation_angle, + stp_filename='EFCCu_coils_2.stp' + ) + EFCCu_coils_3 = paramak.RotateStraightShape( + points=[ + (281.7551963048499, -71.42857142857144), + (289.1454965357968, -71.42857142857144), + (289.1454965357968, -78.80184331797238), + (281.7551963048499, -78.80184331797238), + ], + rotation_angle=rotation_angle, + stp_filename='EFCCu_coils_3.stp' + ) + + EFCCu_coils_4 = paramak.RotateStraightShape( + points=[ + (235.56581986143186, 127.64976958525347), + (240.1847575057737, 121.19815668202767), + (246.65127020785218, 125.80645161290323), + (242.0323325635104, 132.25806451612902), + ], + rotation_angle=rotation_angle, + stp_filename='EFCCu_coils_4.stp' + ) + EFCCu_coils_5 = paramak.RotateStraightShape( + points=[ + (262.3556581986143, 90.78341013824888), + (266.97459584295615, 84.33179723502303), + (273.44110854503464, 88.94009216589859), + (268.82217090069287, 94.47004608294935), + ], + rotation_angle=rotation_angle, + stp_filename='EFCCu_coils_5.stp' + ) + EFCCu_coils_6 = paramak.RotateStraightShape( + points=[ + (281.7551963048499, 71.42857142857144), + (289.1454965357968, 71.42857142857144), + (289.1454965357968, 78.80184331797238), + (281.7551963048499, 78.80184331797238), + ], + rotation_angle=rotation_angle, + stp_filename='EFCCu_coils_6.stp' + ) + + plasma = paramak.Plasma( + major_radius=185, + minor_radius=57 - 6, # 3 is a small ofset to avoid overlaps + triangularity=0.31, + elongation=1.97, + rotation_angle=rotation_angle, + stp_filename='plasma.stp', + ) + + antenna = paramak.RotateMixedShape( + points=[ + (263.2794457274827, 46.5437788018433, 'straight'), + (263.2794457274827, -46.54377880184336, 'straight'), + (231.87066974595842, -46.54377880184336, 'spline'), + (243.87990762124713, 0, 'spline'), + (231.87066974595842, 46.5437788018433, 'straight'), + ], + rotation_angle=rotation_angle, + stp_filename='antenna.stp' + ) + + tf_coil = paramak.ToroidalFieldCoilPrincetonD( + R1=105, + R2=339, + thickness=33, + distance=33, + number_of_coils=12, + rotation_angle=rotation_angle, + stp_filename='tf_coil.stp' + ) + + vac_vessel = paramak.RotateStraightShape( + points=[ + (117.32101616628177, 126.72811059907835), + (163.51039260969978, 170.04608294930875), + (181.98614318706697, 171.88940092165896), + (196.76674364896073, 169.12442396313364), + (196.76674364896073, 115.66820276497694), + (236.4896073903002, 114.74654377880185), + (273.44110854503464, 65.89861751152074), + (272.51732101616625, -65.89861751152074), + (236.4896073903002, -115.66820276497697), + (196.76674364896073, -115.66820276497697), + (196.76674364896073, -169.12442396313367), + (181.98614318706697, -172.81105990783408), + (163.51039260969978, -170.04608294930875), + (117.32101616628177, -126.72811059907832), + (117.32101616628177, 123.04147465437785), + (123.78752886836028, 123.04147465437785), + (123.78752886836028, -123.963133640553), + (165.3579676674365, -162.67281105990781), + (181.98614318706697, -164.5161290322581), + (190.3002309468822, -162.67281105990781), + (190.3002309468822, -112.90322580645159), + (193.99538106235568, -109.21658986175117), + (232.7944572748268, -109.21658986175117), + (266.97459584295615, -63.13364055299536), + (266.05080831408776, 62.21198156682027), + (232.7944572748268, 109.21658986175115), + (193.99538106235568, 109.21658986175115), + (190.3002309468822, 111.98156682027647), + (190.3002309468822, 162.67281105990784), + (181.98614318706697, 164.51612903225805), + (165.3579676674365, 162.67281105990784), + (123.78752886836028, 123.04147465437785), + (117.32101616628177, 123.04147465437785), + ], + rotation_angle=rotation_angle, + stp_filename='vacvessel.stp' + ) + + inner_vessel = paramak.RotateStraightShape( + points=[ + (269.7459584295612, -46.54377880184336, 'straight'), + (231.87066974595842, -46.5437788018433, 'spline'), + (223.55658198614316, -62.21198156682027, 'spline'), + (207.85219399538107, -80.64516129032262, 'spline'), + (166.28175519630486, -115.66820276497697, 'spline'), + (164.43418013856814, -119.35483870967744, 'spline'), + (164.43418013856814, -122.11981566820276, 'straight'), + (173.67205542725173, -140.5529953917051, 'straight'), + (184.75750577367205, -140.5529953917051, 'straight'), + (184.75750577367205, -158.98617511520735, 'straight'), + (181.98614318706697, -159.9078341013825, 'straight'), + (147.80600461893764, -118.43317972350235, 'straight'), + (129.33025404157044, -123.04147465437785, 'straight'), + (145.95842956120094, -111.05990783410135, 'straight'), + (126.55889145496536, -50.23041474654377, 'straight'), + (127.48267898383372, 50.23041474654377, 'straight'), + (145.95842956120094, 110.13824884792626, 'straight'), + (128.40646651270208, 123.04147465437785, 'straight'), + (147.80600461893764, 117.51152073732717, 'straight'), + (181.98614318706697, 159.90783410138246, 'straight'), + (185.6812933025404, 158.98617511520735, 'straight'), + (184.75750577367205, 140.55299539170505, 'straight'), + (172.74826789838338, 140.55299539170505, 'spline'), + (164.43418013856814, 121.19815668202764, 'spline'), + (164.43418013856814, 118.43317972350229, 'spline'), + (165.3579676674365, 115.66820276497694, 'spline'), + (173.67205542725173, 111.05990783410135, 'spline'), + (207.85219399538107, 80.64516129032256, 'spline'), + (220.7852193995381, 66.82027649769586, 'spline'), + (231.87066974595842, 46.5437788018433, 'spline'), + (268.82217090069287, 46.5437788018433, 'straight'), + (268.82217090069287, 63.13364055299536, 'straight'), + (233.71824480369514, 111.98156682027647, 'straight'), + (193.99538106235568, 112.90322580645159, 'straight'), + (192.14780600461893, 164.51612903225805, 'straight'), + (163.51039260969978, 166.35944700460828, 'straight'), + (121.93995381062356, 123.96313364055297, 'straight'), + (121.0161662817552, -125.80645161290323, 'straight'), + (163.51039260969978, -166.35944700460834, 'straight'), + (192.14780600461893, -166.35944700460834, 'straight'), + (193.99538106235568, -112.90322580645159, 'straight'), + (234.64203233256353, -111.9815668202765, 'straight'), + (269.7459584295612, -63.13364055299536, 'straight'), + ], + rotation_angle=rotation_angle, + stp_filename='inner_vessel.stp', + cut=[vac_vessel, vs_coils, antenna] + ) + + sparc = paramak.Reactor([inboard_pf_coils, outboard_pf_coils, + plasma, antenna, vs_coils, inner_vessel, tf_coil, + EFCCu_coils_1, EFCCu_coils_2, EFCCu_coils_3, + EFCCu_coils_4, EFCCu_coils_5, EFCCu_coils_6, + vac_vessel, div_coils]) + + sparc.export_stp() + sparc.export_svg('htc_reactor.svg') + + +if __name__ == "__main__": + main() diff --git a/examples/example_parametric_reactors/make_all_reactors.py b/examples/example_parametric_reactors/make_all_reactors.py new file mode 100644 index 000000000..ed06e6ee6 --- /dev/null +++ b/examples/example_parametric_reactors/make_all_reactors.py @@ -0,0 +1,24 @@ +""" +This python script creates all parametric reactors available and saves stp files +""" + +from ball_reactor import make_ball_reactor +from ball_reactor_single_null import make_ball_reactor_sn +from center_column_study_reactor import make_center_column_study_reactor +from segmented_blanket_ball_reactor import make_ball_reactor_seg +from submersion_reactor import make_submersion +from submersion_reactor_single_null import make_submersion_sn + + +def main(outputs=['stp', 'svg']): + + make_submersion(outputs=outputs) + make_submersion_sn(outputs=outputs) + make_center_column_study_reactor(outputs=outputs) + make_ball_reactor(outputs=outputs) + make_ball_reactor_sn(outputs=outputs) + make_ball_reactor_seg(outputs=outputs) + + +if __name__ == "__main__": + main() diff --git a/examples/example_parametric_reactors/make_animation.py b/examples/example_parametric_reactors/make_animation.py new file mode 100644 index 000000000..4a0b8c041 --- /dev/null +++ b/examples/example_parametric_reactors/make_animation.py @@ -0,0 +1,57 @@ +__doc__ = """ Creates a series of images of a ball reactor images and +combines them into gif animations using the command line tool convert, + part of the imagemagick suite """ + +import argparse +import os +import uuid + +import numpy as np +import paramak +from tqdm import tqdm + +parser = argparse.ArgumentParser() +parser.add_argument("-n", "--number_of_models", type=int, default=10) +args = parser.parse_args() + +for i in tqdm(range(args.number_of_models)): + + my_reactor = paramak.BallReactor( + inner_bore_radial_thickness=50, + inboard_tf_leg_radial_thickness=np.random.uniform(20, 50), + center_column_shield_radial_thickness=np.random.uniform(20, 60), + divertor_radial_thickness=50, + inner_plasma_gap_radial_thickness=50, + plasma_radial_thickness=np.random.uniform(20, 200), + outer_plasma_gap_radial_thickness=50, + firstwall_radial_thickness=5, + blanket_radial_thickness=np.random.uniform(10, 200), + blanket_rear_wall_radial_thickness=10, + elongation=np.random.uniform(1.2, 1.7), + triangularity=np.random.uniform(0.3, 0.55), + number_of_tf_coils=16, + rotation_angle=180, + pf_coil_radial_thicknesses=[50, 50, 50, 50], + pf_coil_vertical_thicknesses=[50, 50, 50, 50], + pf_coil_to_rear_blanket_radial_gap=50, + pf_coil_to_tf_coil_radial_gap=50, + outboard_tf_coil_radial_thickness=100, + outboard_tf_coil_poloidal_thickness=50, + ) + + my_reactor.export_2d_image( + filename="output_for_animation_2d/" + str(uuid.uuid4()) + ".png" + ) + my_reactor.export_svg( + filename="output_for_animation_svg/" + str(uuid.uuid4()) + ".svg" + ) + + print(str(args.number_of_models), "models made") + +os.system("convert -delay 40 output_for_animation_2d/*.png 2d.gif") + +os.system("convert -delay 40 output_for_animation_3d/*.png 3d.gif") + +os.system("convert -delay 40 output_for_animation_svg/*.svg 3d_svg.gif") + +print("animation file made 2d.gif, 3d.gif and 3d_svg.gif") diff --git a/examples/example_parametric_reactors/segmented_blanket_ball_reactor.py b/examples/example_parametric_reactors/segmented_blanket_ball_reactor.py new file mode 100644 index 000000000..45741fe4f --- /dev/null +++ b/examples/example_parametric_reactors/segmented_blanket_ball_reactor.py @@ -0,0 +1,70 @@ +""" +This example creates a ball reactor with segmented blankets using the +SegmentedBlanketBallReactor parametric reactor. By default the script saves +stp, stl, html and svg files. Fillets the firstwall as this is not currently +done in the SegmentedBlanketBallReactor reactor class. +""" + +import cadquery as cq +import paramak + + +def make_ball_reactor_seg(outputs=['stp', 'neutronics', 'svg', 'stl', 'html']): + + my_reactor = paramak.SegmentedBlanketBallReactor( + inner_bore_radial_thickness=5, + inboard_tf_leg_radial_thickness=25, + center_column_shield_radial_thickness=45, + divertor_radial_thickness=150, + inner_plasma_gap_radial_thickness=50, + plasma_radial_thickness=300, + outer_plasma_gap_radial_thickness=50, + firstwall_radial_thickness=15, + blanket_radial_thickness=50, + blanket_rear_wall_radial_thickness=30, + elongation=2, + triangularity=0.55, + number_of_tf_coils=16, + rotation_angle=180, + pf_coil_radial_thicknesses=[50, 50, 50, 50], + pf_coil_vertical_thicknesses=[50, 50, 50, 50], + pf_coil_to_rear_blanket_radial_gap=50, + pf_coil_to_tf_coil_radial_gap=50, + outboard_tf_coil_radial_thickness=100, + outboard_tf_coil_poloidal_thickness=50, + gap_between_blankets=30, + number_of_blanket_segments=15, + blanket_fillet_radius=15, + ) + + # finds the correct edges to fillet + x = my_reactor.major_radius + front_face = my_reactor._blanket.solid.faces( + cq.NearestToPointSelector((x, 0, 0))) + front_edge = front_face.edges(cq.NearestToPointSelector((x, 0, 0))) + front_edge_length = front_edge.val().Length() + my_reactor._blanket.solid = my_reactor._blanket.solid.edges( + paramak.EdgeLengthSelector(front_edge_length)).fillet( + my_reactor.blanket_fillet_radius) + + # cuts away the breeder zone + my_reactor._blanket.solid = my_reactor._blanket.solid.cut( + my_reactor._blanket.solid) + + my_reactor._blanket.export_stp('firstwall_with_fillet.stp') + + if 'stp' in outputs: + my_reactor.export_stp(output_folder='SegmentedBlanketBallReactor') + if 'neutronics' in outputs: + my_reactor.export_neutronics_description( + 'SegmentedBlanketBallReactor/manifest.json') + if 'svg' in outputs: + my_reactor.export_svg('SegmentedBlanketBallReactor/reactor.svg') + if 'stl' in outputs: + my_reactor.export_stl(output_folder='SegmentedBlanketBallReactor') + if 'html' in outputs: + my_reactor.export_html('SegmentedBlanketBallReactor/reactor.html') + + +if __name__ == "__main__": + make_ball_reactor_seg(['stp', 'neutronics', 'svg', 'stl', 'html']) diff --git a/examples/example_parametric_reactors/submersion_reactor.py b/examples/example_parametric_reactors/submersion_reactor.py new file mode 100644 index 000000000..b87f5b032 --- /dev/null +++ b/examples/example_parametric_reactors/submersion_reactor.py @@ -0,0 +1,50 @@ +""" +This example creates a submersion reactor using the SubmersionTokamak +parametric reactor. By default the script saves stp, stl, html and svg files. +""" + +import paramak + + +def make_submersion(outputs=['stp', 'neutronics', 'svg', 'stl', 'html']): + + my_reactor = paramak.SubmersionTokamak( + inner_bore_radial_thickness=30, + inboard_tf_leg_radial_thickness=30, + center_column_shield_radial_thickness=30, + divertor_radial_thickness=80, + inner_plasma_gap_radial_thickness=50, + plasma_radial_thickness=200, + outer_plasma_gap_radial_thickness=50, + firstwall_radial_thickness=30, + blanket_rear_wall_radial_thickness=30, + number_of_tf_coils=16, + rotation_angle=180, + support_radial_thickness=90, + inboard_blanket_radial_thickness=30, + outboard_blanket_radial_thickness=30, + elongation=2.00, + triangularity=0.50, + pf_coil_radial_thicknesses=[30, 30, 30, 30], + pf_coil_vertical_thicknesses=[30, 30, 30, 30], + pf_coil_to_tf_coil_radial_gap=50, + outboard_tf_coil_radial_thickness=30, + outboard_tf_coil_poloidal_thickness=30, + tf_coil_to_rear_blanket_radial_gap=20, + ) + + if 'stp' in outputs: + my_reactor.export_stp(output_folder='SubmersionTokamak') + if 'neutronics' in outputs: + my_reactor.export_neutronics_description( + 'SubmersionTokamak/manifest.json') + if 'svg' in outputs: + my_reactor.export_svg('SubmersionTokamak/reactor.svg') + if 'stl' in outputs: + my_reactor.export_stl(output_folder='SubmersionTokamak') + if 'html' in outputs: + my_reactor.export_html('SubmersionTokamak/reactor.html') + + +if __name__ == "__main__": + make_submersion(['stp', 'neutronics', 'svg', 'stl', 'html']) diff --git a/examples/example_parametric_reactors/submersion_reactor_single_null.py b/examples/example_parametric_reactors/submersion_reactor_single_null.py new file mode 100644 index 000000000..88bef870f --- /dev/null +++ b/examples/example_parametric_reactors/submersion_reactor_single_null.py @@ -0,0 +1,56 @@ +""" +This example creates a single null submersion reactor using the +SubmersionTokamak parametric reactor. By default the script saves stp, stl, +html and svg files. +""" + +from pathlib import Path + +import paramak + + +def make_submersion_sn(outputs=['stp', 'neutronics', 'svg', 'stl', 'html'], + output_folder='SubmersionTokamak_sn'): + + my_reactor = paramak.SingleNullSubmersionTokamak( + inner_bore_radial_thickness=30, + inboard_tf_leg_radial_thickness=30, + center_column_shield_radial_thickness=30, + divertor_radial_thickness=80, + inner_plasma_gap_radial_thickness=50, + plasma_radial_thickness=200, + outer_plasma_gap_radial_thickness=50, + firstwall_radial_thickness=30, + blanket_rear_wall_radial_thickness=30, + number_of_tf_coils=16, + rotation_angle=180, + support_radial_thickness=90, + inboard_blanket_radial_thickness=30, + outboard_blanket_radial_thickness=30, + elongation=2.00, + triangularity=0.50, + pf_coil_radial_thicknesses=[30, 30, 30, 30], + pf_coil_vertical_thicknesses=[30, 30, 30, 30], + pf_coil_to_tf_coil_radial_gap=50, + outboard_tf_coil_radial_thickness=30, + outboard_tf_coil_poloidal_thickness=30, + tf_coil_to_rear_blanket_radial_gap=20, + divertor_position="lower", + support_position="lower" + ) + + if 'stp' in outputs: + my_reactor.export_stp(output_folder=output_folder) + if 'neutronics' in outputs: + my_reactor.export_neutronics_description( + Path(output_folder) / 'manifest.json') + if 'svg' in outputs: + my_reactor.export_svg(Path(output_folder) / 'reactor.svg') + if 'stl' in outputs: + my_reactor.export_stl(output_folder=output_folder) + if 'html' in outputs: + my_reactor.export_html(Path(output_folder) / 'reactor.html') + + +if __name__ == "__main__": + make_submersion_sn(['stp', 'neutronics', 'svg', 'stl', 'html']) diff --git a/examples/example_parametric_shapes/make_CAD_from_points.ipynb b/examples/example_parametric_shapes/make_CAD_from_points.ipynb new file mode 100644 index 000000000..4e6583206 --- /dev/null +++ b/examples/example_parametric_shapes/make_CAD_from_points.ipynb @@ -0,0 +1,221 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This python script demonstrates the creation of 3D volumes from points using extrude and rotate methods" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import paramak\n", + "\n", + "rotated_straights = paramak.RotateStraightShape(\n", + " points=[\n", + " (400, 100),\n", + " (400, 200),\n", + " (600, 200),\n", + " (600, 100)\n", + " ],\n", + " rotation_angle = 180\n", + ")\n", + "\n", + "rotated_straights.solid" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "rotated_spline = paramak.RotateSplineShape(\n", + " points=[\n", + " (500, 0),\n", + " (500, -20),\n", + " (400, -300),\n", + " (300, -300),\n", + " (400, 0),\n", + " (300, 300),\n", + " (400, 300),\n", + " (500, 20),\n", + " ],\n", + "rotation_angle = 180\n", + ")\n", + "\n", + "rotated_spline.solid" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "rotated_mixed = paramak.RotateMixedShape(\n", + " points=[\n", + " (100, 0, 'straight'),\n", + " (200, 0, 'circle'),\n", + " (250, 50, 'circle'),\n", + " (200, 100, 'straight'),\n", + " (150, 100, 'spline'),\n", + " (140, 75, 'spline'),\n", + " (110, 45, 'spline'),\n", + " ],\n", + " rotation_angle = 180\n", + ")\n", + "\n", + "rotated_mixed.solid" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This makes a circular shape and rotates it to make a solid" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "rotated_circle = paramak.RotateCircleShape(\n", + " points=[(50, 0)],\n", + " radius=5,\n", + " rotation_angle=180\n", + ")\n", + "\n", + "rotated_circle.solid" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "rotated_circle = paramak.ExtrudeCircleShape(\n", + " points=[(50, 0)],\n", + " radius=5,\n", + " distance=15\n", + ")\n", + "\n", + "rotated_circle.solid" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This makes a banana shape with straight edges and extrudes it to make a solid" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "extruded_straight = paramak.ExtrudeStraightShape(\n", + " points=[\n", + " (300, -300),\n", + " (400, 0),\n", + " (300, 300),\n", + " (400, 300),\n", + " (500, 0),\n", + " (400, -300),\n", + " ],\n", + " distance=200\n", + ")\n", + "extruded_straight.solid" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This makes a banana shape and rotates it to make a solid" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "extruded_spline = paramak.ExtrudeSplineShape(\n", + " points=[\n", + " (500, 0),\n", + " (500, -20),\n", + " (400, -300),\n", + " (300, -300),\n", + " (400, 0),\n", + " (300, 300),\n", + " (400, 300),\n", + " (500, 20),\n", + " ],\n", + " distance=200,\n", + ")\n", + "\n", + "extruded_spline.solid" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This makes a shape with straight, circular and spline edges and extrudes it to make a solid" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "extruded_mixed = paramak.ExtrudeMixedShape(\n", + " points=[\n", + " (100, 0, 'straight'),\n", + " (200, 0, 'circle'),\n", + " (250, 50, 'circle'),\n", + " (200, 100, 'straight'),\n", + " (150, 100, 'spline'),\n", + " (140, 75, 'spline'),\n", + " (110, 45, 'spline'),\n", + " ],\n", + " distance=200\n", + ")\n", + "\n", + "extruded_mixed.solid" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/example_parametric_shapes/make_CAD_from_points.py b/examples/example_parametric_shapes/make_CAD_from_points.py new file mode 100644 index 000000000..00d215dc5 --- /dev/null +++ b/examples/example_parametric_shapes/make_CAD_from_points.py @@ -0,0 +1,221 @@ +""" +This python script demonstrates the creation of 3D volumes from points using +extrude and rotate methods +""" + +import paramak + + +def main(): + + # rotate examples + + # this makes a rectangle and rotates it to make a solid + rotated_straights = paramak.RotateStraightShape( + rotation_angle=180, + points=[(400, 100), (400, 200), (600, 200), (600, 100)] + ) + rotated_straights.export_stp("rotated_straights.stp") + rotated_straights.export_html("rotated_straights.html") + + # this makes a banana shape and rotates it to make a solid + rotated_spline = paramak.RotateSplineShape( + rotation_angle=180, + points=[ + (500, 0), + (500, -20), + (400, -300), + (300, -300), + (400, 0), + (300, 300), + (400, 300), + (500, 20), + ] + ) + rotated_spline.export_stp("rotated_spline.stp") + rotated_spline.export_html("rotated_spline.html") + + # this makes a shape with straight, spline and circular edges and rotates + # it to make a solid + rotated_mixed = paramak.RotateMixedShape( + rotation_angle=180, + points=[ + (100, 0, "straight"), + (200, 0, "circle"), + (250, 50, "circle"), + (200, 100, "straight"), + (150, 100, "spline"), + (140, 75, "spline"), + (110, 45, "spline"), + ] + ) + rotated_mixed.export_stp("rotated_mixed.stp") + rotated_mixed.export_html("rotated_mixed.html") + + # this makes a circular shape and rotates it to make a solid + rotated_circle = paramak.RotateCircleShape( + rotation_angle=180, + points=[(50, 0)], + radius=5, + workplane="XZ" + ) + rotated_circle.export_stp("rotated_circle.stp") + rotated_circle.export_html("rotated_circle.html") + + # extrude examples + + # this makes a banana shape with straight edges and rotates it to make a + # solid + extruded_straight = paramak.ExtrudeStraightShape( + distance=200, + points=[ + (300, -300), + (400, 0), + (300, 300), + (400, 300), + (500, 0), + (400, -300), + ] + ) + extruded_straight.export_stp("extruded_straight.stp") + extruded_straight.export_html("extruded_straight.html") + + # this makes a banana shape and rotates it to make a solid + extruded_spline = paramak.ExtrudeSplineShape( + distance=200, + points=[ + (500, 0), + (500, -20), + (400, -300), + (300, -300), + (400, 0), + (300, 300), + (400, 300), + (500, 20), + ] + ) + extruded_spline.export_stp("extruded_spline.stp") + extruded_spline.export_html("extruded_spline.html") + + # this makes a banana shape straight top and bottom edges and extrudes it + # to make a solid + extruded_mixed = paramak.ExtrudeMixedShape( + distance=100, + points=[ + (100, 0, "straight"), + (200, 0, "circle"), + (250, 50, "circle"), + (200, 100, "straight"), + (150, 100, "spline"), + (140, 75, "spline"), + (110, 45, "spline"), + ], + ) + extruded_mixed.export_stp("extruded_mixed.stp") + extruded_mixed.export_html("extruded_mixed.html") + + # this makes a circular shape and extrudes it to make a solid + extruded_circle = paramak.ExtrudeCircleShape( + points=[(20, 0)], + radius=20, + distance=200 + ) + extruded_circle.export_stp("extruded_circle.stp") + extruded_circle.export_html("extruded_circle.html") + + # sweep examples + + # this makes a banana shape with straight edges and sweeps it along a + # spline to make a solid + sweep_straight = paramak.SweepStraightShape( + points=[ + (-150, 300), + (-50, 300), + (50, 0), + (-50, -300), + (-150, -300), + (-50, 0) + ], + path_points=[ + (50, 0), + (150, 400), + (400, 500), + (650, 600), + (750, 1000) + ], + workplane="XY", + path_workplane="XZ" + ) + sweep_straight.export_stp("sweep_straight.stp") + sweep_straight.export_html("sweep_straight.html") + + # this makes a banana shape with spline edges and sweeps it along a spline + # to make a solid + sweep_spline = paramak.SweepSplineShape( + points=[ + (50, 0), + (50, -20), + (-50, -300), + (-150, -300), + (-50, 0), + (-150, 300), + (-50, 300), + (50, 20) + ], + path_points=[ + (50, 0), + (150, 400), + (400, 500), + (650, 600), + (750, 1000) + ], + workplane="XY", + path_workplane="XZ" + ) + sweep_spline.export_stp("sweep_spline.stp") + sweep_spline.export_html("sweep_spline.html") + + # this makes a shape with straight, spline and circular edges and sweeps + # it along a spline to make a solid + sweep_mixed = paramak.SweepMixedShape( + points=[ + (-80, -50, "straight"), + (20, -50, "circle"), + (70, 0, "circle"), + (20, 50, "straight"), + (-30, 50, "spline"), + (-40, 25, "spline"), + (-70, -5, "spline") + ], + path_points=[ + (50, 0), + (150, 400), + (400, 500), + (650, 600), + (750, 1000) + ], + workplane="XY", + path_workplane="XZ" + ) + sweep_mixed.export_stp("sweep_mixed.stp") + sweep_mixed.export_html("sweep_mixed.html") + + # this makes a circular shape and sweeps it to make a solid + sweep_circle = paramak.SweepCircleShape( + radius=40, + path_points=[ + (50, 0), + (150, 400), + (400, 500), + (650, 600), + (750, 1000) + ], + workplane="XY", + path_workplane="XZ" + ) + sweep_circle.export_stp("sweep_circle.stp") + sweep_circle.export_html("sweep_circle.html") + + +if __name__ == "__main__": + main() diff --git a/examples/example_parametric_shapes/make_blanket_from_parameters.py b/examples/example_parametric_shapes/make_blanket_from_parameters.py new file mode 100644 index 000000000..a3de71b95 --- /dev/null +++ b/examples/example_parametric_shapes/make_blanket_from_parameters.py @@ -0,0 +1,31 @@ +""" +This python script demonstrates the parametric creation of a shape similar to +a breeder blanket. +""" + +import paramak + + +def main(filename="blanket_from_parameters.stp"): + + height = 700 + blanket_rear = 400 + blanket_front = 300 + blanket_mid_point = 350 + + blanket = paramak.RotateMixedShape( + rotation_angle=180, + points=[ + (blanket_rear, height / 2.0, "straight"), + (blanket_rear, -height / 2.0, "straight"), + (blanket_front, -height / 2.0, "spline"), + (blanket_mid_point, 0, "spline"), + (blanket_front, height / 2.0, "straight"), + ] + ) + + blanket.export_stp(filename=filename) + + +if __name__ == "__main__": + main() diff --git a/examples/example_parametric_shapes/make_blanket_from_points.py b/examples/example_parametric_shapes/make_blanket_from_points.py new file mode 100644 index 000000000..7001a5081 --- /dev/null +++ b/examples/example_parametric_shapes/make_blanket_from_points.py @@ -0,0 +1,25 @@ +""" +This python script demonstrates the creation of a breeder blanket from points +""" + +import paramak + + +def main(filename="blanket_from_points.stp"): + + blanket = paramak.RotateMixedShape( + rotation_angle=180, + points=[ + (538, 305, "straight"), + (538, -305, "straight"), + (322, -305, "spline"), + (470, 0, "spline"), + (322, 305, "straight"), + ] + ) + + blanket.export_stp(filename=filename) + + +if __name__ == "__main__": + main() diff --git a/examples/example_parametric_shapes/make_can_reactor_from_parameters.py b/examples/example_parametric_shapes/make_can_reactor_from_parameters.py new file mode 100644 index 000000000..7f9e96b37 --- /dev/null +++ b/examples/example_parametric_shapes/make_can_reactor_from_parameters.py @@ -0,0 +1,135 @@ +""" +This script creates a can shaped reactor with plasma, center column, blanket, firstwall, divertor and core +""" + +import paramak + + +def main(): + + outer_most_x = 900 + blanket_height = 300 + + plasma = paramak.Plasma( + major_radius=250, + minor_radius=100, + triangularity=0.5, + elongation=2.5, + rotation_angle=180 + ) + + centre_column = paramak.RotateMixedShape( + rotation_angle=180, + points=[ + (74.6, 687.0, "straight"), + (171.0, 687.0, "straight"), + (171.0, 459.9232, "spline"), + (108.001, 249.9402, "spline"), + (92.8995, 0, "spline"), + (108.001, -249.9402, "spline"), + (171.0, -459.9232, "straight"), + (171.0, -687.0, "straight"), + (74.6, -687.0, "straight"), + ] + ) + centre_column.stp_filename = "centre_column.stp" + centre_column.stl_filename = "centre_column.stl" + + blanket = paramak.RotateMixedShape( + rotation_angle=180, + points=[ + (325.4528, blanket_height, "straight"), + (outer_most_x, blanket_height, "straight"), + (outer_most_x, -blanket_height, "straight"), + (325.4528, -blanket_height, "spline"), + (389.9263, -138.1335, "spline"), + (404.5108, 0, "spline"), + (389.9263, 138.1335, "spline"), + ] + ) + blanket.stp_filename = "blanket.stp" + blanket.stl_filename = "blanket.stl" + + firstwall = paramak.RotateMixedShape( + rotation_angle=180, + points=[ + (322.9528, blanket_height, "straight"), + (325.4528, blanket_height, "spline"), + (389.9263, 138.1335, "spline"), + (404.5108, 0, "spline"), + (389.9263, -138.1335, "spline"), + (325.4528, -blanket_height, "straight"), + (322.9528, -blanket_height, "spline"), + (387.4263, -138.1335, "spline"), + (402.0108, 0, "spline"), + (387.4263, 138.1335, "spline"), + ] + ) + firstwall.stp_filename = "firstwall.stp" + firstwall.stl_filename = "firstwall.stl" + + divertor_bottom = paramak.RotateMixedShape( + rotation_angle=180, + points=[ + (192.4782, -447.204, "spline"), + (272.4957, -370.5, "spline"), + (322.9528, -blanket_height, "straight"), + (outer_most_x, -blanket_height, "straight"), + (outer_most_x, -687.0, "straight"), + (171.0, -687.0, "straight"), + (171.0, -459.9232, "spline"), + (218.8746, -513.3484, "spline"), + (362.4986, -602.3905, "straight"), + (372.5012, -580.5742, "spline"), + (237.48395, -497.21782, "spline"), + ] + ) + divertor_bottom.stp_filename = "divertor_bottom.stp" + divertor_bottom.stl_filename = "divertor_bottom.stl" + + divertor_top = paramak.RotateMixedShape( + rotation_angle=180, + points=[ + (192.4782, 447.204, "spline"), + (272.4957, 370.5, "spline"), + (322.9528, blanket_height, "straight"), + (outer_most_x, blanket_height, "straight"), + (outer_most_x, 687.0, "straight"), + (171.0, 687.0, "straight"), + (171.0, 459.9232, "spline"), + (218.8746, 513.3484, "spline"), + (362.4986, 602.3905, "straight"), + (372.5012, 580.5742, "spline"), + (237.48395, 497.21782, "spline"), + ] + ) + divertor_top.stp_filename = "divertor_top.stp" + divertor_top.stl_filename = "divertor_top.stl" + + core = paramak.RotateStraightShape( + rotation_angle=180, + points=[ + (0, 687.0), + (74.6, 687.0), + (74.6, -687.0), + (0, -687.0)] + ) + core.stp_filename = "core.stp" + core.stl_filename = "core.stl" + + # initiates a reactor object + myreactor = paramak.Reactor([plasma, + blanket, + core, + divertor_top, + divertor_bottom, + firstwall, + centre_column]) + + myreactor.export_stp(output_folder="can_reactor_from_parameters") + myreactor.export_stl(output_folder="can_reactor_from_parameters") + myreactor.export_html(filename="can_reactor_from_parameters/reactor.html") + + +if __name__ == "__main__": + main() diff --git a/examples/example_parametric_shapes/make_can_reactor_from_points.py b/examples/example_parametric_shapes/make_can_reactor_from_points.py new file mode 100644 index 000000000..7f4a8d3fd --- /dev/null +++ b/examples/example_parametric_shapes/make_can_reactor_from_points.py @@ -0,0 +1,128 @@ +""" +This python script demonstrates the creation of 3D volumes +from points to create an example reactor +""" + +import paramak + + +def main(): + + plasma = paramak.Plasma( + major_radius=250, + minor_radius=100, + triangularity=0.5, + elongation=2.5, + rotation_angle=180, + ) + + centre_column = paramak.RotateMixedShape( + rotation_angle=180, + points=[ + (74.6, 687.0, "straight"), + (171.0, 687.0, "straight"), + (171.0, 459.9232, "spline"), + (108.001, 249.9402, "spline"), + (92.8995, 0, "spline"), + (108.001, -249.9402, "spline"), + (171.0, -459.9232, "straight"), + (171.0, -687.0, "straight"), + (74.6, -687.0, "straight"), + ] + ) + centre_column.stp_filename = "centre_column.stp" + centre_column.stl_filename = "centre_column.stl" + + blanket = paramak.RotateMixedShape( + rotation_angle=180, + points=[ + (325.4886, 300.5, "straight"), + (538.4886, 300.5, "straight"), + (538.4886, -300.5, "straight"), + (325.4528, -300.5, "spline"), + (389.9263, -138.1335, "spline"), + (404.5108, 0, "spline"), + (389.9263, 138.1335, "spline"), + ] + ) + blanket.stp_filename = "blanket.stp" + blanket.stl_filename = "blanket.stl" + + firstwall = paramak.RotateMixedShape( + rotation_angle=180, + points=[ + (322.9528, 300.5, "straight"), + (325.4528, 300.5, "spline"), + (389.9263, 138.1335, "spline"), + (404.5108, 0, "spline"), + (389.9263, -138.1335, "spline"), + (325.4528, -300.5, "straight"), + (322.9528, -300.5, "spline"), + (387.4263, -138.1335, "spline"), + (402.0108, 0, "spline"), + (387.4263, 138.1335, "spline"), + ] + ) + firstwall.stp_filename = "firstwall.stp" + firstwall.stl_filename = "firstwall.stl" + + divertor_bottom = paramak.RotateMixedShape( + rotation_angle=180, + points=[ + (192.4782, -447.204, "spline"), + (272.4957, -370.5, "spline"), + (322.9528, -300.5, "straight"), + (538.4886, -300.5, "straight"), + (538.4886, -687.0, "straight"), + (171.0, -687.0, "straight"), + (171.0, -459.9232, "spline"), + (218.8746, -513.3484, "spline"), + (362.4986, -602.3905, "straight"), + (372.5012, -580.5742, "spline"), + (237.48395, -497.21782, "spline"), + ] + ) + divertor_bottom.stp_filename = "divertor_bottom.stp" + divertor_bottom.stl_filename = "divertor_bottom.stl" + + divertor_top = paramak.RotateMixedShape( + rotation_angle=180, + points=[ + (192.4782, 447.204, "spline"), + (272.4957, 370.5, "spline"), + (322.9528, 300.5, "straight"), + (538.4886, 300.5, "straight"), + (538.4886, 687.0, "straight"), + (171.0, 687.0, "straight"), + (171.0, 459.9232, "spline"), + (218.8746, 513.3484, "spline"), + (362.4986, 602.3905, "straight"), + (372.5012, 580.5742, "spline"), + (237.48395, 497.21782, "spline"), + ] + ) + divertor_top.stp_filename = "divertor_top.stp" + divertor_top.stl_filename = "divertor_top.stl" + + core = paramak.RotateStraightShape( + rotation_angle=180, + points=[(0, 687.0), (74.6, 687.0), (74.6, -687.0), (0, -687.0)] + ) + core.stp_filename = "core.stp" + core.stl_filename = "core.stl" + + myreactor = paramak.Reactor([plasma, + blanket, + core, + divertor_top, + divertor_bottom, + firstwall, + centre_column]) + + myreactor.export_stp(output_folder="can_reactor_from_points") + myreactor.export_stl(output_folder="can_reactor_from_points") + myreactor.export_html("can_reactor_from_points/reactor.html") + + +if __name__ == "__main__": + main() diff --git a/meta.yaml b/meta.yaml new file mode 100644 index 000000000..e609011f0 --- /dev/null +++ b/meta.yaml @@ -0,0 +1,61 @@ +{% set name = "paramak" %} +{% set version = "0.0.16" %} + +package: + name: "{{ name|lower }}" + version: "{{ version }}" + +source: + url: "https://pypi.io/packages/source/{{ name[0] }}/{{ name }}/{{ name }}-{{ version }}.tar.gz" + sha256: 815145a891ad8cab0ddd114dcdbc879522c2b66e287ea3d5d960601510211dbd + +build: + number: 0 + # script: "{{ PYTHON }} -m pip install . -vv" + +requirements: + host: + - matplotlib + - numpy + - pip + - plasmaboundaries + - plotly + - pytest-cov + - python + - scipy + - sympy + - cadquery + run: + - matplotlib + - numpy + - plasmaboundaries + - plotly + - pytest-cov + - python + - scipy + - sympy + - cadquery + +test: + imports: + - paramak + - paramak.parametric_components + - paramak.parametric_reactors + - paramak.parametric_shapes + - tests + requires: + - pytest-cov + - pytest-runner + +about: + home: "https://github.com/ukaea/paramak" + license: MIT + license_family: MIT + license_file: LICENSE.txt + summary: "Create 3D fusion reactor CAD models based on input parameters" + doc_url: https://paramak.readthedocs.io/ + dev_url: https://github.com/ukaea/paramak + +extra: + recipe-maintainers: + - shimwell diff --git a/optional-requirements.txt b/optional-requirements.txt new file mode 100644 index 000000000..26f37520c --- /dev/null +++ b/optional-requirements.txt @@ -0,0 +1,2 @@ +neutronics_material_maker +parametric_plasma_source diff --git a/paramak/__init__.py b/paramak/__init__.py new file mode 100644 index 000000000..6e27e09f2 --- /dev/null +++ b/paramak/__init__.py @@ -0,0 +1,82 @@ +from .shape import Shape +from .reactor import Reactor +from .utils import rotate, extend, distance_between_two_points, diff_between_angles +from .utils import EdgeLengthSelector, FaceAreaSelector +from .neutronics_utils import define_moab_core_and_tags, add_stl_to_moab_core + +from .parametric_shapes.extruded_mixed_shape import ExtrudeMixedShape +from .parametric_shapes.extruded_spline_shape import ExtrudeSplineShape +from .parametric_shapes.extruded_straight_shape import ExtrudeStraightShape +from .parametric_shapes.extruded_circle_shape import ExtrudeCircleShape + +from .parametric_shapes.rotate_mixed_shape import RotateMixedShape +from .parametric_shapes.rotate_spline_shape import RotateSplineShape +from .parametric_shapes.rotate_straight_shape import RotateStraightShape +from .parametric_shapes.rotate_circle_shape import RotateCircleShape + +from .parametric_shapes.sweep_mixed_shape import SweepMixedShape +from .parametric_shapes.sweep_spline_shape import SweepSplineShape +from .parametric_shapes.sweep_straight_shape import SweepStraightShape +from .parametric_shapes.sweep_circle_shape import SweepCircleShape + +from .parametric_components.tokamak_plasma import Plasma +from .parametric_components.tokamak_plasma_from_points import PlasmaFromPoints +from .parametric_components.tokamak_plasma_plasmaboundaries import PlasmaBoundaries + +from .parametric_components.blanket_constant_thickness_arc_h import BlanketConstantThicknessArcH +from .parametric_components.blanket_constant_thickness_arc_v import BlanketConstantThicknessArcV +from .parametric_components.blanket_fp import BlanketFP +from .parametric_components.blanket_poloidal_segment import BlanketFPPoloidalSegments + +from .parametric_components.divertor_ITER import ITERtypeDivertor +from .parametric_components.divertor_ITER_no_dome import ITERtypeDivertorNoDome + +from .parametric_components.center_column_cylinder import CenterColumnShieldCylinder +from .parametric_components.center_column_hyperbola import CenterColumnShieldHyperbola +from .parametric_components.center_column_flat_top_hyperbola import CenterColumnShieldFlatTopHyperbola +from .parametric_components.center_column_plasma_dependant import CenterColumnShieldPlasmaHyperbola +from .parametric_components.center_column_circular import CenterColumnShieldCircular +from .parametric_components.center_column_flat_top_circular import CenterColumnShieldFlatTopCircular + +from .parametric_components.coolant_channel_ring_straight import CoolantChannelRingStraight +from .parametric_components.coolant_channel_ring_curved import CoolantChannelRingCurved + +from .parametric_components.inboard_firstwall_fccs import InboardFirstwallFCCS + +from .parametric_components.poloidal_field_coil import PoloidalFieldCoil +from .parametric_components.poloidal_field_coil_fp import PoloidalFieldCoilFP +from .parametric_components.poloidal_field_coil_case import PoloidalFieldCoilCase +from .parametric_components.poloidal_field_coil_case_fc import PoloidalFieldCoilCaseFC +from .parametric_components.poloidal_field_coil_set import PoloidalFieldCoilSet +from .parametric_components.poloidal_field_coil_case_set import PoloidalFieldCoilCaseSet +from .parametric_components.poloidal_field_coil_case_set_fc import PoloidalFieldCoilCaseSetFC + +from .parametric_components.poloidal_segmenter import PoloidalSegments +from .parametric_components.port_cutters_rotated import PortCutterRotated +from .parametric_components.port_cutters_rectangular import PortCutterRectangular +from .parametric_components.port_cutters_circular import PortCutterCircular +from .parametric_components.cutting_wedge import CuttingWedge +from .parametric_components.cutting_wedge_fs import CuttingWedgeFS +from .parametric_components.blanket_cutter_parallels import BlanketCutterParallels +from .parametric_components.blanket_cutters_star import BlanketCutterStar + +from .parametric_components.inner_tf_coils_circular import InnerTfCoilsCircular +from .parametric_components.inner_tf_coils_flat import InnerTfCoilsFlat + +from .parametric_components.toroidal_field_coil_coat_hanger import ToroidalFieldCoilCoatHanger +from .parametric_components.toroidal_field_coil_rectangle import ToroidalFieldCoilRectangle +from .parametric_components.toroidal_field_coil_triple_arc import ToroidalFieldCoilTripleArc +from .parametric_components.toroidal_field_coil_princeton_d import ToroidalFieldCoilPrincetonD +from .parametric_components.tf_coil_casing import TFCoilCasing + +from .parametric_components.vacuum_vessel import VacuumVessel +from .parametric_components.hollow_cube import HollowCube + +from .parametric_reactors.ball_reactor import BallReactor +from .parametric_reactors.submersion_reactor import SubmersionTokamak +from .parametric_reactors.single_null_submersion_reactor import SingleNullSubmersionTokamak +from .parametric_reactors.single_null_ball_reactor import SingleNullBallReactor +from .parametric_reactors.segmented_blanket_ball_reactor import SegmentedBlanketBallReactor +from .parametric_reactors.center_column_study_reactor import CenterColumnStudyReactor + +from .parametric_neutronics.neutronics_model_from_reactor import NeutronicsModelFromReactor diff --git a/paramak/neutronics_utils.py b/paramak/neutronics_utils.py new file mode 100644 index 000000000..e003ec7cc --- /dev/null +++ b/paramak/neutronics_utils.py @@ -0,0 +1,121 @@ + +import numpy as np + + +def define_moab_core_and_tags(): + """Creates a MOAB Core instance which can be built up by adding sets of + triangles to the instance + + Returns: + (pymoab Core): A pymoab.core.Core() instance + (pymoab tag_handle): A pymoab.core.tag_get_handle() instance + """ + + try: + from pymoab import core, types + except ImportError as err: + raise err('PyMoab not found, export_h5m method not available') + + # create pymoab instance + moab_core = core.Core() + + tags = dict() + + sense_tag_name = "GEOM_SENSE_2" + sense_tag_size = 2 + tags['surf_sense'] = moab_core.tag_get_handle( + sense_tag_name, + sense_tag_size, + types.MB_TYPE_HANDLE, + types.MB_TAG_SPARSE, + create_if_missing=True) + + tags['category'] = moab_core.tag_get_handle( + types.CATEGORY_TAG_NAME, + types.CATEGORY_TAG_SIZE, + types.MB_TYPE_OPAQUE, + types.MB_TAG_SPARSE, + create_if_missing=True) + tags['name'] = moab_core.tag_get_handle( + types.NAME_TAG_NAME, + types.NAME_TAG_SIZE, + types.MB_TYPE_OPAQUE, + types.MB_TAG_SPARSE, + create_if_missing=True) + tags['geom_dimension'] = moab_core.tag_get_handle( + types.GEOM_DIMENSION_TAG_NAME, + 1, + types.MB_TYPE_INTEGER, + types.MB_TAG_DENSE, + create_if_missing=True) + + # Global ID is a default tag, just need the name to retrieve + tags['global_id'] = moab_core.tag_get_handle(types.GLOBAL_ID_TAG_NAME) + + return moab_core, tags + + +def add_stl_to_moab_core( + moab_core, + surface_id, + volume_id, + material_name, + tags, + stl_filename): + """Computes the m and c coefficients of the equation (y=mx+c) for + a straight line from two points. + + Args: + moab_core (pymoab.core.Core): + surface_id (int): the id number to apply to the surface + volume_id (int): the id numbers to apply to the volumes + material_name (str): the material tag name to add. Will be prepended + with mat: + tags (pymoab tag_handle): the MOAB tags + stl_filename (str): the filename of the stl file to load into the moab + core + + Returns: + (pymoab Core): An updated pymoab.core.Core() instance + """ + + surface_set = moab_core.create_meshset() + volume_set = moab_core.create_meshset() + + # recent versions of MOAB handle this automatically + # but best to go ahead and do it manually + moab_core.tag_set_data(tags['global_id'], volume_set, volume_id) + + moab_core.tag_set_data(tags['global_id'], surface_set, surface_id) + + # set geom IDs + moab_core.tag_set_data(tags['geom_dimension'], volume_set, 3) + moab_core.tag_set_data(tags['geom_dimension'], surface_set, 2) + + # set category tag values + moab_core.tag_set_data(tags['category'], volume_set, "Volume") + moab_core.tag_set_data(tags['category'], surface_set, "Surface") + + # establish parent-child relationship + moab_core.add_parent_child(volume_set, surface_set) + + # set surface sense + sense_data = [volume_set, np.uint64(0)] + moab_core.tag_set_data(tags['surf_sense'], surface_set, sense_data) + + # load the stl triangles/vertices into the surface set + moab_core.load_file(stl_filename, surface_set) + + group_set = moab_core.create_meshset() + moab_core.tag_set_data(tags['category'], group_set, "Group") + print("mat:{}".format(material_name)) + moab_core.tag_set_data( + tags['name'], + group_set, + "mat:{}".format(material_name)) + moab_core.tag_set_data(tags['geom_dimension'], group_set, 4) + + # add the volume to this group set + moab_core.add_entity(group_set, volume_set) + + return moab_core diff --git a/paramak/parametric_components/__init__.py b/paramak/parametric_components/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/paramak/parametric_components/blanket_constant_thickness_arc_h.py b/paramak/parametric_components/blanket_constant_thickness_arc_h.py new file mode 100644 index 000000000..51a674160 --- /dev/null +++ b/paramak/parametric_components/blanket_constant_thickness_arc_h.py @@ -0,0 +1,70 @@ + +from paramak import RotateMixedShape + + +class BlanketConstantThicknessArcH(RotateMixedShape): + """An outboard blanket volume that follows the curvature of a circular + arc and a constant blanket thickness. The upper and lower edges continue + horizontally for the thickness of the blanket to back of the blanket. + + Arguments: + inner_mid_point ((float, float)): the x,z coordinates of the mid + point on the inner surface of the blanket. + inner_upper_point ((float, float)): the x,z coordinates of the upper + point on the inner surface of the blanket. + inner_lower_point ((float, float)): the x,z coordinates of the lower + point on the inner surface of the blanket. + thickness (float): the radial thickness of the blanket in cm. + stp_filename (str, optional): Defaults to + "BlanketConstantThicknessArcH.stp". + stl_filename (str, optional): Defaults to + "BlanketConstantThicknessArcH.stl". + material_tag (str, optional): Defaults to "blanket_mat". + """ + + def __init__( + self, + inner_mid_point, + inner_upper_point, + inner_lower_point, + thickness, + stp_filename="BlanketConstantThicknessArcH.stp", + stl_filename="BlanketConstantThicknessArcH.stl", + material_tag="blanket_mat", + **kwargs + ): + + super().__init__( + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + self.inner_upper_point = inner_upper_point + self.inner_lower_point = inner_lower_point + self.inner_mid_point = inner_mid_point + self.thickness = thickness + + def find_points(self): + + self.points = [ + (self.inner_upper_point[0], self.inner_upper_point[1], "circle"), + (self.inner_mid_point[0], self.inner_mid_point[1], "circle"), + (self.inner_lower_point[0], self.inner_lower_point[1], "straight"), + ( + self.inner_lower_point[0] + abs(self.thickness), + self.inner_lower_point[1], + "circle", + ), + ( + self.inner_mid_point[0] + abs(self.thickness), + self.inner_mid_point[1], + "circle", + ), + ( + self.inner_upper_point[0] + abs(self.thickness), + self.inner_upper_point[1], + "straight", + ) + ] diff --git a/paramak/parametric_components/blanket_constant_thickness_arc_v.py b/paramak/parametric_components/blanket_constant_thickness_arc_v.py new file mode 100644 index 000000000..c14e85cc5 --- /dev/null +++ b/paramak/parametric_components/blanket_constant_thickness_arc_v.py @@ -0,0 +1,70 @@ + +from paramak import RotateMixedShape + + +class BlanketConstantThicknessArcV(RotateMixedShape): + """An outboard blanket volume that follows the curvature of a circular + arc and a constant blanket thickness. The upper and lower edges continue + vertically for the thickness of the blanket to back of the blanket. + + Arguments: + inner_mid_point ((float, float)): the x,z coordinates of the mid + point on the inner surface of the blanket. + inner_upper_point ((float, float)): the x,z coordinates of the upper + point on the inner surface of the blanket. + inner_lower_point ((float, float)): the x,z coordinates of the lower + point on the inner surface of the blanket. + thickness (float): the radial thickness of the blanket in cm. + stp_filename (str, optional): Defaults to + "BlanketConstantThicknessArcV.stp". + stl_filename (str, optional): Defaults to + "BlanketConstantThicknessArcV.stl". + material_tag (str, optional): Defaults to "blanket_mat". + """ + + def __init__( + self, + inner_mid_point, + inner_upper_point, + inner_lower_point, + thickness, + stp_filename="BlanketConstantThicknessArcV.stp", + stl_filename="BlanketConstantThicknessArcV.stl", + material_tag="blanket_mat", + **kwargs + ): + + super().__init__( + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + self.inner_upper_point = inner_upper_point + self.inner_lower_point = inner_lower_point + self.inner_mid_point = inner_mid_point + self.thickness = thickness + + def find_points(self): + + self.points = [ + (self.inner_upper_point[0], self.inner_upper_point[1], "circle"), + (self.inner_mid_point[0], self.inner_mid_point[1], "circle"), + (self.inner_lower_point[0], self.inner_lower_point[1], "straight"), + ( + self.inner_lower_point[0], + self.inner_lower_point[1] - abs(self.thickness), + "circle", + ), + ( + self.inner_mid_point[0] + self.thickness, + self.inner_mid_point[1], + "circle", + ), + ( + self.inner_upper_point[0], + self.inner_upper_point[1] + abs(self.thickness), + "straight", + ) + ] diff --git a/paramak/parametric_components/blanket_cutter_parallels.py b/paramak/parametric_components/blanket_cutter_parallels.py new file mode 100644 index 000000000..c96972661 --- /dev/null +++ b/paramak/parametric_components/blanket_cutter_parallels.py @@ -0,0 +1,108 @@ + +from paramak import ExtrudeStraightShape +from paramak.utils import cut_solid + + +class BlanketCutterParallels(ExtrudeStraightShape): + """Creates an extruded shape with a parallel rectangular section repeated + around the reactor. The shape is used to cut other components (eg. blankets + and firstwalls) in order to create a banana section of the blankets with + parrallel sides.Typically used to divide a blanket into vertical + sections with a fixed distance between each section. + + Args: + thickness (float): extruded distance (cm) of the cutter which + translates to being the gap size between blankets when the cutter + is used to segment blankets. + gap_size (float): the distance between the inner edges of the two + parrallel extrusions + height (float, optional): height (cm) of the port. Defaults to 2000.0. + width (float, optional): width (cm) of the port. Defaults to 2000.0. + azimuth_placement_angle (list or float, optional): Defaults + to [0., 36., 72., 108., 144., 180., 216., 252., 288., 324.] + stp_filename (str, optional): Defaults to "BlanketCutterParallels.stp". + stl_filename (str, optional): Defaults to "BlanketCutterParallels.stl". + name (str, optional): Defaults to "blanket_cutter_Parallels". + material_tag (str, optional): Defaults to + "blanket_cutter_parallels_mat". + """ + + def __init__( + self, + thickness, + gap_size, + height=2000., + width=2000., + azimuth_placement_angle=[0., 36., 72., 108., 144., 180., 216., 252., + 288., 324.], + stp_filename="BlanketCutterParallels.stp", + stl_filename="BlanketCutterParallels.stl", + name="blanket_cutter_parallels", + material_tag="blanket_cutter_parallels_mat", + **kwargs + ): + self.main_cutting_shape = \ + ExtrudeStraightShape( + distance=gap_size / 2.0, + azimuth_placement_angle=azimuth_placement_angle, + ) + self.gap_size = gap_size + self.thickness = thickness + super().__init__( + distance=self.distance, + azimuth_placement_angle=azimuth_placement_angle, + stp_filename=stp_filename, + stl_filename=stl_filename, + name=name, + material_tag=material_tag, + **kwargs + ) + self.height = height + self.width = width + + @property + def distance(self): + self.distance = self.gap_size / 2.0 + self.thickness + return self._distance + + @distance.setter + def distance(self, value): + self._distance = value + + @property + def gap_size(self): + return self._gap_size + + @gap_size.setter + def gap_size(self, value): + self.main_cutting_shape.distance = value / 2.0 + self._gap_size = value + + @property + def azimuth_placement_angle(self): + return self._azimuth_placement_angle + + @azimuth_placement_angle.setter + def azimuth_placement_angle(self, value): + self.main_cutting_shape.azimuth_placement_angle = value + self._azimuth_placement_angle = value + + def find_points(self): + + points = [ + (0, -self.height / 2), + (self.width, -self.height / 2), + (self.width, self.height / 2), + (0, self.height / 2) + ] + + self.main_cutting_shape.points = points + + self.points = points[:-1] + + def create_solid(self): + solid = super().create_solid() + solid = cut_solid(solid, self.main_cutting_shape) + self.solid = solid + + return solid diff --git a/paramak/parametric_components/blanket_cutters_star.py b/paramak/parametric_components/blanket_cutters_star.py new file mode 100644 index 000000000..ce276e24f --- /dev/null +++ b/paramak/parametric_components/blanket_cutters_star.py @@ -0,0 +1,64 @@ + +from paramak import ExtrudeStraightShape + + +class BlanketCutterStar(ExtrudeStraightShape): + """Creates an extruded shape with a rectangular section that is used to cut + other components (eg. blankets and firstwalls) in order to create banana + stlye blanket segments. Typically used to divide a blanket into vertical + sections with a fixed gap between each section. + + Args: + distance (float): extruded distance (cm) of the cutter which translates + to being the gap size between blankets when the cutter is used to + segment blankets. + height (float, optional): height (cm) of the port. Defaults to 2000.0. + width (float, optional): width (cm) of the port. Defaults to 2000.0. + azimuth_placement_angle (list or float, optional): Defaults + to [0., 36., 72., 108., 144., 180., 216., 252., 288., 324.] + stp_filename (str, optional): Defaults to "BlanketCutterStar.stp". + stl_filename (str, optional): Defaults to "BlanketCutterStar.stl". + name (str, optional): defaults to "blanket_cutter_star". + material_tag (str, optional): Defaults to + "blanket_cutter_star_mat". + """ + + def __init__( + self, + distance, + height=2000., + width=2000., + azimuth_placement_angle=[0., 36., 72., 108., 144., 180., 216., 252., + 288., 324.], + stp_filename="BlanketCutterStar.stp", + stl_filename="BlanketCutterStar.stl", + name="blanket_cutter_star", + material_tag="blanket_cutter_star_mat", + **kwargs + ): + + super().__init__( + extrude_both=True, + name=name, + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + azimuth_placement_angle=azimuth_placement_angle, + distance=distance, + **kwargs + ) + + self.azimuth_placement_angle = azimuth_placement_angle + self.height = height + self.width = width + self.distance = distance + + def find_points(self): + + points = [ + (0, -self.height / 2), + (self.width, -self.height / 2), + (self.width, self.height / 2), + (0, self.height / 2), + ] + self.points = points diff --git a/paramak/parametric_components/blanket_fp.py b/paramak/parametric_components/blanket_fp.py new file mode 100644 index 000000000..5ea75cb13 --- /dev/null +++ b/paramak/parametric_components/blanket_fp.py @@ -0,0 +1,364 @@ + +import warnings + +import mpmath + +import numpy as np +import sympy as sp +from paramak import RotateMixedShape, diff_between_angles +from scipy.interpolate import interp1d + + +class BlanketFP(RotateMixedShape): + """A blanket volume created from plasma parameters. + + Args: + thickness (float or [float] or callable or [(float), (float)]): + the thickness of the blanket (cm). If the thickness is a float then + this produces a blanket of constant thickness. If the thickness is + a tuple of floats, blanket thickness will vary linearly between the + two values. If thickness is callable, then the blanket thickness + will be a function of poloidal angle (in degrees). If thickness is + a list of two lists (thicknesses and angles) then these will be + used together with linear interpolation. + start_angle (float): the angle in degrees to start the blanket, + measured anti clockwise from 3 o'clock. + stop_angle (float): the angle in degrees to stop the blanket, measured + anti clockwise from 3 o'clock. + plasma (paramak.Plasma, optional): If not None, the parameters of the + plasma Object will be used. Defaults to None. + minor_radius (float, optional): the minor radius of the plasma (cm). + Defaults to 150.0. + major_radius (float, optional): the major radius of the plasma (cm). + Defaults to 450.0. + triangularity (float, optional): the triangularity of the plasma. + Defaults to 0.55. + elongation (float, optional): the elongation of the plasma. Defaults + to 2.0. + vertical_displacement (float, optional): the vertical_displacement of + the plasma (cm). Defaults to 0. + offset_from_plasma (float, optional): the distance between the plasma + and the blanket (cm). If float, constant offset. If list of floats, + offset will vary linearly between the values. If callable, offset + will be a function of poloidal angle (in degrees). If a list of + two lists (offsets and angles) then these will be used together + with linear interpolation. Defaults to 0.0. + num_points (int, optional): number of points that will describe the + shape. Defaults to 50. + stp_filename (str, optional): Defaults to "BlanketFP.stp". + stl_filename (str, optional): Defaults to "BlanketFP.stl". + material_tag (str, optional): Defaults to "blanket_mat". + """ + + def __init__( + self, + thickness, + start_angle, + stop_angle, + plasma=None, + minor_radius=150.0, + major_radius=450.0, + triangularity=0.55, + elongation=2.0, + vertical_displacement=0.0, + offset_from_plasma=0.0, + num_points=50, + stp_filename="BlanketFP.stp", + stl_filename="BlanketFP.stl", + material_tag="blanket_mat", + **kwargs + ): + + super().__init__( + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + self.thickness = thickness + self.start_angle, self.stop_angle = None, None + self.start_angle = start_angle + self.stop_angle = stop_angle + self.plasma = plasma + self.vertical_displacement = vertical_displacement + if plasma is None: + self.minor_radius = minor_radius + self.major_radius = major_radius + self.triangularity = triangularity + self.elongation = elongation + else: # if plasma object is given, use its parameters + self.minor_radius = plasma.minor_radius + self.major_radius = plasma.major_radius + self.triangularity = plasma.triangularity + self.elongation = plasma.elongation + self.offset_from_plasma = offset_from_plasma + self.num_points = num_points + self.physical_groups = None + + @property + def start_angle(self): + return self._start_angle + + @start_angle.setter + def start_angle(self, value): + self._start_angle = value + + @property + def stop_angle(self): + return self._stop_angle + + @stop_angle.setter + def stop_angle(self, value): + self._stop_angle = value + + @property + def physical_groups(self): + self.create_physical_groups() + return self._physical_groups + + @physical_groups.setter + def physical_groups(self, physical_groups): + self._physical_groups = physical_groups + + @property + def minor_radius(self): + return self._minor_radius + + @minor_radius.setter + def minor_radius(self, minor_radius): + self._minor_radius = minor_radius + + @property + def thickness(self): + return self._thickness + + @thickness.setter + def thickness(self, thickness): + self._thickness = thickness + + @property + def inner_points(self): + self.find_points() + return self._inner_points + + @inner_points.setter + def inner_points(self, value): + self._inner_points = value + + @property + def outer_points(self): + self.find_points() + return self._outer_points + + @outer_points.setter + def outer_points(self, value): + self._outer_points = value + + def make_callable(self, attribute): + """This function transforms an attribute (thickness or offset) into a + callable function of theta + """ + # if the attribute is a list, create a interpolated object of the + # values + if isinstance(attribute, (tuple, list)): + if isinstance(attribute[0], (tuple, list)) and \ + isinstance(attribute[1], (tuple, list)) and \ + len(attribute) == 2: + # attribute is a list of 2 lists + if len(attribute[0]) != len(attribute[1]): + raise ValueError('The length of angles list must equal \ + the length of values list') + list_of_angles = np.array(attribute[0]) + offset_values = attribute[1] + else: + # no list of angles is given + offset_values = attribute + list_of_angles = np.linspace( + self.start_angle, + self.stop_angle, + len(offset_values), + endpoint=True) + interpolated_values = interp1d(list_of_angles, offset_values) + + def fun(theta): + if callable(attribute): + return attribute(theta) + elif isinstance(attribute, (tuple, list)): + return interpolated_values(theta) + else: + return attribute + return fun + + def find_points(self, angles=None): + self._overlapping_shape = False + # create array of angles theta + if angles is None: + thetas = np.linspace( + self.start_angle, + self.stop_angle, + num=self.num_points, + endpoint=True, + ) + else: + thetas = angles + + # create inner points + inner_offset = self.make_callable(self.offset_from_plasma) + inner_points = self.create_offset_points(thetas, inner_offset) + inner_points[-1][2] = "straight" + self.inner_points = inner_points + + # create outer points + thickness = self.make_callable(self.thickness) + + def outer_offset(theta): + return inner_offset(theta) + thickness(theta) + + outer_points = self.create_offset_points(np.flip(thetas), outer_offset) + outer_points[-1][2] = "straight" + self.outer_points = outer_points + + # assemble + points = inner_points + outer_points + if self._overlapping_shape: + msg = "BlanketFP: Some points with negative R" + \ + " coordinate have been ignored." + warnings.warn(msg) + + self.points = points + return points + + def create_offset_points(self, thetas, offset): + """generates a list of points following parametric equations with an + offset + + Args: + thetas (np.array): the angles in degrees. + offset (callable): offset value (cm). offset=0 will follow the + parametric equations. + + Returns: + list: list of points [[R1, Z1, connection1], [R2, Z2, connection2], + ...] + """ + # create sympy objects and derivatives + theta_sp = sp.Symbol("theta") + + R_sp, Z_sp = self.distribution(theta_sp, pkg=sp) + R_derivative = sp.diff(R_sp, theta_sp) + Z_derivative = sp.diff(Z_sp, theta_sp) + points = [] + + for theta in thetas: + # get local value of derivatives + val_R_derivative = float(R_derivative.subs("theta", theta)) + val_Z_derivative = float(Z_derivative.subs("theta", theta)) + + # get normal vector components + nx = val_Z_derivative + ny = -val_R_derivative + + # normalise normal vector + normal_vector_norm = (nx ** 2 + ny ** 2) ** 0.5 + nx /= normal_vector_norm + ny /= normal_vector_norm + + # calculate outer points + val_R_outer = self.distribution(theta)[0] + offset(theta) * nx + val_Z_outer = self.distribution(theta)[1] + offset(theta) * ny + if float(val_R_outer) > 0: + points.append( + [float(val_R_outer), float(val_Z_outer), "spline"]) + else: + self._overlapping_shape = True + return points + + def create_physical_groups(self): + """Creates the physical groups for STP files + + Returns: + list: list of dicts containing the physical groups + """ + + groups = [] + nb_volumes = 1 # only one volume + nb_surfaces = 2 # inner and outer + + surface_names = ["inner", "outer"] + volumes_names = ["inside"] + + # add two cut sections if they exist + if self.rotation_angle != 360: + nb_surfaces += 2 + surface_names += ["left_section", "right_section"] + full_rot = False + else: + full_rot = True + + # add two surfaces between blanket and div if they exist + if diff_between_angles(self.start_angle, self.stop_angle) != 0: + nb_surfaces += 2 + surface_names += ["inner_section", "outer_section"] + stop_equals_start = False + else: + stop_equals_start = True + + # rearrange order + new_order = [i for i in range(len(surface_names))] + if full_rot: + if not stop_equals_start: + # from ["inner", "outer", "inner_section", "outer_section"] + + # to ["inner", "inner_section", "outer", "outer_section"] + new_order = [0, 2, 1, 3] + else: + if stop_equals_start: + print( + "Warning: If start_angle = stop_angle surfaces will not\ + be handled correctly" + ) + new_order = [0, 1, 2, 3] + else: + # from ['inner', 'outer', 'left_section', 'right_section', + # 'inner_section', 'outer_section'] + + # to ["inner", "inner_section", "outer", "outer_section", + # "left_section", "right_section"] + new_order = [0, 4, 1, 5, 2, 3] + surface_names = [surface_names[i] for i in new_order] + + for i in range(1, nb_volumes + 1): + group = {"dim": 3, "id": i, "name": volumes_names[i - 1]} + groups.append(group) + for i in range(1, nb_surfaces + 1): + group = {"dim": 2, "id": i, "name": surface_names[i - 1]} + groups.append(group) + self.physical_groups = groups + + def distribution(self, theta, pkg=np): + """Plasma distribution theta in degrees + + Args: + theta (float or np.array or sp.Symbol): the angle(s) in degrees. + pkg (module, optional): Module to use in the funciton. If sp, as + sympy object will be returned. If np, a np.array or a float + will be returned. Defaults to np. + + Returns: + (float, float) or (sympy.Add, sympy.Mul) or + (numpy.array, numpy.array): The R and Z coordinates of the + point with angle theta + """ + if pkg == np: + theta = np.radians(theta) + else: + theta = mpmath.radians(theta) + R = self.major_radius + self.minor_radius * pkg.cos( + theta + self.triangularity * pkg.sin(theta) + ) + Z = ( + self.elongation * self.minor_radius * pkg.sin(theta) + + self.vertical_displacement + ) + return R, Z diff --git a/paramak/parametric_components/blanket_poloidal_segment.py b/paramak/parametric_components/blanket_poloidal_segment.py new file mode 100644 index 000000000..6034cbe1d --- /dev/null +++ b/paramak/parametric_components/blanket_poloidal_segment.py @@ -0,0 +1,311 @@ + +import warnings + +import numpy as np +from paramak import BlanketFP, RotateStraightShape +from paramak.utils import (cut_solid, distance_between_two_points, extend, + rotate) +from scipy.optimize import minimize + + +class BlanketFPPoloidalSegments(BlanketFP): + """Poloidally segmented Blanket inheriting from paramak.BlanketFP. + + Args: + segments_angles (list, optional): If not None, the segments ends will + be located at these angles. If None and if the constraints + length_limits and nb_segments_limits are not None, segments angles + will be linearly distributed. Else, an optimum configuration + meeting the set requirements will be found. Defaults to None. + num_segments (int, optional): Number of segments (igored if + segments_angles is not None). Defaults to 7. + length_limits ((float, float), optional): The minimum and maximum + acceptable length of the segments. Ex: (100, 500), (100, None), + (None, 300), None, (None, None). Defaults to None. + nb_segments_limits ((float, float), optional): The minimum and maximum + acceptable number of segments. Ex: (3, 10), (5, None), (None, 7), + None, (None, None). Defaults to None. + segments_gap (float, optional): Distance between segments. Defaults to + 0.0. + """ + + def __init__( + self, + segments_angles=None, + num_segments=7, + length_limits=None, + nb_segments_limits=None, + segments_gap=0.0, + **kwargs + ): + super().__init__( + **kwargs + ) + self.num_segments = num_segments + self.length_limits = length_limits + self.nb_segments_limits = nb_segments_limits + self.segments_angles = segments_angles + self.segments_gap = segments_gap + self.segments_cutters = None + + @property + def segments_angles(self): + return self._segments_angles + + @segments_angles.setter + def segments_angles(self, value): + if value is not None: + if self.start_angle is not None or self.stop_angle is not None: + msg = "start_angle and stop_angle attributes will be " + \ + "ignored if segments_angles is not None" + warnings.warn(msg, UserWarning) + elif self.num_segments is not None: + msg = "num_segment attribute will be ignored if " + \ + "segments_angles is not None" + warnings.warn(msg, UserWarning) + self._segments_angles = value + + @property + def num_segments(self): + return self._num_segments + + @num_segments.setter + def num_segments(self, value): + if value is not None: + self.num_points = value + 1 + self._num_segments = value + + @property + def segments_cutters(self): + self.create_segment_cutters() + return self._segments_cutters + + @segments_cutters.setter + def segments_cutters(self, value): + self._segments_cutters = value + + def get_angles(self): + """Get the poloidal angles of the segments. + + Returns: + list: the angles + """ + if (self.length_limits, self.nb_segments_limits) != (None, None): + angles = segments_optimiser( + self.length_limits, self.nb_segments_limits, + self.distribution, (self.start_angle, self.stop_angle), + stop_on_success=True + ) + elif self.segments_angles is None: + angles = np.linspace( + self.start_angle, self.stop_angle, + num=self.num_segments + 1) + else: + angles = self.segments_angles + return angles + + def find_points(self): + points = super().find_points(angles=self.get_angles()) + + # every points straight connections + for point in points: + point[-1] = 'straight' + self.points = points[:-1] + + def create_solid(self): + solid = super().create_solid() + segments_cutters = self.segments_cutters + if segments_cutters is not None: + solid = cut_solid(solid, segments_cutters) + self.solid = solid + return solid + + def create_segment_cutters(self): + """Creates a shape for cutting the blanket into segments and store it + in segments_cutter attribute + """ + if self.segments_gap > 0: + # initialise main cutting shape + cutting_shape = RotateStraightShape( + rotation_angle=self.rotation_angle, + azimuth_placement_angle=self.azimuth_placement_angle, + union=[]) + # add points to the shape to avoid void solid + cutting_shape.points = [ + (self.major_radius, + self.vertical_displacement), + (self.major_radius + + self.minor_radius / + 10, + self.vertical_displacement), + (self.major_radius + + self.minor_radius / + 10, + self.vertical_displacement + + self.minor_radius / + 10), + (self.major_radius, + self.vertical_displacement + + self.minor_radius / + 10), + ] + + # Create cutters for each gap + for inner_point, outer_point in zip( + self.inner_points[:-1], + self.outer_points[-1::-1]): + # initialise cutter for gap + cutter = RotateStraightShape( + rotation_angle=self.rotation_angle, + azimuth_placement_angle=self.azimuth_placement_angle + ) + # create rectangle of dimension |AB|*2.6 x self.segments_gap + A = (inner_point[0], inner_point[1]) + B = (outer_point[0], outer_point[1]) + + # increase rectangle length + security_factor = 0.8 + local_thickness = distance_between_two_points(A, B) + A = extend(A, B, -local_thickness * security_factor) + B = extend(A, B, local_thickness * (1 + 2 * security_factor)) + # create points for cutter + points_cutter = [ + A, + B, + rotate( + B, + extend( + B, + A, + self.segments_gap), + angle=-np.pi / 2), + rotate( + A, + extend( + A, + B, + self.segments_gap), + angle=np.pi / 2)] + cutter.points = points_cutter + # add cutter to global cutting shape + cutting_shape.union.append(cutter) + + self.segments_cutters = cutting_shape + + +def compute_lengths_from_angles(angles, distribution): + """Computes the length of segments between a set of points on a (x,y) + distribution. + + Args: + angles (list): Contains the angles of the points (degree) + distribution (callable): function taking an angle as argument and + returning (x,y) coordinates. + + Returns: + list: contains the lengths of the segments. + """ + points = [] + for angle in angles: + points.append(distribution(angle)) + + lengths = [] + for i in range(len(points) - 1): + lengths.append(distance_between_two_points(points[i], points[i + 1])) + return lengths + + +def segments_optimiser(length_limits, nb_segments_limits, distribution, angles, + stop_on_success=True): + """Optimiser segmenting a given R(theta), Z(theta) distribution of points + with constraints regarding the number of segments and the length of the + segments. + + Args: + length_limits ((float, float)): The minimum and maximum acceptable + length of the segments. Ex: (100, 500), (100, None), (None, 300), + None, (None, None) + nb_segments_limits ((int, int)): The minimum and maximum acceptable + number of segments. Ex: (3, 10), (5, None), (None, 7), + None, (None, None) + distribution (callable): function taking an angle as argument and + returning (x,y) coordinates. + angles ((float, float)): the start and stop angles of the distribution. + stop_on_sucess (bool, optional): If set to True, the optimiser will + stop as soon as a configuration meets the requirements. + + Returns: + list: list of optimised angles + """ + if length_limits is None: + min_length, max_length = None, None + else: + min_length, max_length = length_limits + + if nb_segments_limits is None: + min_nb_segments, max_nb_segments = None, None + else: + min_nb_segments, max_nb_segments = nb_segments_limits + + if min_length is None: + min_length = 0 + if max_length is None: + max_length = float('inf') + if min_nb_segments is None: + min_nb_segments = 1 + if max_nb_segments is None: + max_nb_segments = 50 + + start_angle, stop_angle = angles + + # define cost function + def cost_function(angles): + angles_with_extremums = [start_angle] + \ + [angle for angle in angles] + [stop_angle] + + lengths = compute_lengths_from_angles( + angles_with_extremums, distribution) + + cost = 0 + for length in lengths: + if not min_length <= length <= max_length: + cost += min(abs(min_length - length), abs(max_length - length)) + return cost + + # test for several numbers of segments the best config + best = [float("inf"), []] + + for nb_segments in range(min_nb_segments, max_nb_segments + 1): + # initialise angles to linspace + list_of_angles = \ + np.linspace(start_angle, stop_angle, num=nb_segments + 1) + + # use scipy minimize to find best set of angles + res = minimize( + cost_function, list_of_angles[1:-1], method="Nelder-Mead") + + # complete the optimised angles with extrema + optimised_angles = [start_angle] + \ + [angle for angle in res.x] + [stop_angle] + + # check that the optimised angles meet the lengths requirements + lengths = compute_lengths_from_angles(optimised_angles, distribution) + break_the_rules = False + for length in lengths: + if not min_length <= length <= max_length: + break_the_rules = True + break + if not break_the_rules: + # compare with previous results and get the minimum + # cost function value + best = min(best, [res.fun, optimised_angles], key=lambda x: x[0]) + if stop_on_success: + return optimised_angles + + # return the results + returned_angles = best[1] + if returned_angles == []: + msg = "Couldn't find optimum configuration for Blanket segments" + raise ValueError(msg) + + return returned_angles diff --git a/paramak/parametric_components/center_column_circular.py b/paramak/parametric_components/center_column_circular.py new file mode 100644 index 000000000..b29793ba0 --- /dev/null +++ b/paramak/parametric_components/center_column_circular.py @@ -0,0 +1,95 @@ + +from paramak import RotateMixedShape + + +class CenterColumnShieldCircular(RotateMixedShape): + """A center column shield volume with a circular outer profile and constant + cylindrical inner profile. + + Args: + height (float): height of the center column shield (cm). + inner_radius (float): the inner radius of the center column shield + (cm). + mid_radius (float): the inner radius of the outer hyperbolic profile of + the center colunn shield (cm). + outer_radius (float): the outer radius of the center column shield. + stp_filename (str, optional): Defaults to + "CenterColumnShieldCircular.stp". + stl_filename (str, optional): Defaults to + "CenterColumnShieldCircular.stl". + name (str, optional): Defaults to "center_column_shield". + material_tag (str, optional): Defaults to + "center_column_shield_mat". + """ + + def __init__( + self, + height, + inner_radius, + mid_radius, + outer_radius, + stp_filename="CenterColumnShieldCircular.stp", + stl_filename="CenterColumnShieldCircular.stl", + name="center_column_shield", + material_tag="center_column_shield_mat", + **kwargs + ): + + super().__init__( + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + self.height = height + self.inner_radius = inner_radius + self.mid_radius = mid_radius + self.outer_radius = outer_radius + + @property + def height(self): + return self._height + + @height.setter + def height(self, height): + self._height = height + + @property + def inner_radius(self): + return self._inner_radius + + @inner_radius.setter + def inner_radius(self, inner_radius): + self._inner_radius = inner_radius + + @property + def mid_radius(self): + return self._mid_radius + + @mid_radius.setter + def mid_radius(self, mid_radius): + self._mid_radius = mid_radius + + @property + def outer_radius(self): + return self._outer_radius + + @outer_radius.setter + def outer_radius(self, outer_radius): + self._outer_radius = outer_radius + + def find_points(self): + """Finds the XZ points and connection types (straight and circle) that + describe the 2D profile of the center column shield shape.""" + + points = [ + (self.inner_radius, 0, "straight"), + (self.inner_radius, self.height / 2, "straight"), + (self.outer_radius, self.height / 2, "circle"), + (self.mid_radius, 0, "circle"), + (self.outer_radius, -self.height / 2, "straight"), + (self.inner_radius, -self.height / 2, "straight") + ] + + self.points = points diff --git a/paramak/parametric_components/center_column_cylinder.py b/paramak/parametric_components/center_column_cylinder.py new file mode 100644 index 000000000..c102ae424 --- /dev/null +++ b/paramak/parametric_components/center_column_cylinder.py @@ -0,0 +1,89 @@ + +from paramak import RotateStraightShape + + +class CenterColumnShieldCylinder(RotateStraightShape): + """A cylindrical center column shield volume with constant thickness. + + Arguments: + height (float): height of the center column shield. + inner_radius (float): the inner radius of the center column shield. + outer_radius (float): the outer radius of the center column shield. + stp_filename (str, optional): Defaults to + "CenterColumnShieldCylinder.stp". + stl_filename (str, optional): Defaults to + "CenterColumnShieldCylinder.stl". + material_tag (str, optional): Defaults to "center_column_shield_mat". + """ + + def __init__( + self, + height, + inner_radius, + outer_radius, + stp_filename="CenterColumnShieldCylinder.stp", + stl_filename="CenterColumnShieldCylinder.stl", + material_tag="center_column_shield_mat", + **kwargs + ): + + super().__init__( + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + self.height = height + self.inner_radius = inner_radius + self.outer_radius = outer_radius + + @property + def height(self): + return self._height + + @height.setter + def height(self, value): + if value is None: + raise ValueError( + "height of the CenterColumnShieldBlock cannot be None") + self._height = value + + @property + def inner_radius(self): + return self._inner_radius + + @inner_radius.setter + def inner_radius(self, value): + if hasattr(self, "outer_radius"): + if value >= self.outer_radius: + raise ValueError( + "inner_radius ({}) is larger than outer_radius ({})".format( + value, self.outer_radius)) + self._inner_radius = value + + @property + def outer_radius(self): + return self._outer_radius + + @outer_radius.setter + def outer_radius(self, value): + if hasattr(self, "inner_radius"): + if value <= self.inner_radius: + raise ValueError( + "inner_radius ({}) is larger than outer_radius ({})".format( + self.inner_radius, value)) + self._outer_radius = value + + def find_points(self): + """Finds the XZ points joined by straight connections that describe the + 2D profile of the center column shield shape.""" + + points = [ + (self.inner_radius, self.height / 2), + (self.outer_radius, self.height / 2), + (self.outer_radius, -self.height / 2), + (self.inner_radius, -self.height / 2), + ] + + self.points = points diff --git a/paramak/parametric_components/center_column_flat_top_circular.py b/paramak/parametric_components/center_column_flat_top_circular.py new file mode 100644 index 000000000..f5db9b557 --- /dev/null +++ b/paramak/parametric_components/center_column_flat_top_circular.py @@ -0,0 +1,109 @@ + +from paramak import RotateMixedShape + + +class CenterColumnShieldFlatTopCircular(RotateMixedShape): + """A center column shield volume with a circular outer profile joined to + flat profiles at the top and bottom of the shield, and a constant + cylindrical inner profile. + + Args: + height (float): height of the center column shield. + arc_height (float): height of the outer circular profile of the center + column shield. + inner_radius (float): the inner radius of the center column shield. + mid_radius (float): the inner radius of the outer circular profile of + the center column shield. + outer_radius (float): the outer radius of the center column shield. + stp_filename (str, optional): Defaults to + "CenterColumnShieldFlatTopCircular.stp". + stl_filename (str, optional): Defaults to + "CenterColumnShieldFlatTopCircular.stl". + name (str, optional): Defaults to "center_column". + material_tag (str, optional): Defaults to "center_column_shield_mat". + """ + + def __init__( + self, + height, + arc_height, + inner_radius, + mid_radius, + outer_radius, + stp_filename="CenterColumnShieldFlatTopCircular.stp", + stl_filename="CenterColumnShieldFlatTopCircular.stl", + name="center_column", + material_tag="center_column_shield_mat", + **kwargs + ): + + super().__init__( + name=name, + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + self.height = height + self.arc_height = arc_height + self.mid_radius = mid_radius + self.outer_radius = outer_radius + self.inner_radius = inner_radius + + @property + def height(self): + return self._height + + @height.setter + def height(self, height): + self._height = height + + @property + def arc_height(self): + return self._arc_height + + @arc_height.setter + def arc_height(self, arc_height): + self._arc_height = arc_height + + @property + def inner_radius(self): + return self._inner_radius + + @inner_radius.setter + def inner_radius(self, inner_radius): + self._inner_radius = inner_radius + + @property + def mid_radius(self): + return self._mid_radius + + @mid_radius.setter + def mid_radius(self, mid_radius): + self._mid_radius = mid_radius + + @property + def outer_radius(self): + return self._outer_radius + + @outer_radius.setter + def outer_radius(self, outer_radius): + self._outer_radius = outer_radius + + def find_points(self): + """Finds the XZ points and connection types (straight and circle) that + describe the 2D profile of the center column shield shape.""" + + points = [ + (self.inner_radius, 0, "straight"), + (self.inner_radius, self.height / 2, "straight"), + (self.outer_radius, self.height / 2, "straight"), + (self.outer_radius, self.arc_height / 2, "circle"), + (self.mid_radius, 0, "circle"), + (self.outer_radius, -self.arc_height / 2, "straight"), + (self.outer_radius, -self.height / 2, "straight"), + (self.inner_radius, -self.height / 2, "straight") + ] + + self.points = points diff --git a/paramak/parametric_components/center_column_flat_top_hyperbola.py b/paramak/parametric_components/center_column_flat_top_hyperbola.py new file mode 100644 index 000000000..c9d2f17e5 --- /dev/null +++ b/paramak/parametric_components/center_column_flat_top_hyperbola.py @@ -0,0 +1,120 @@ + +from paramak import RotateMixedShape + + +class CenterColumnShieldFlatTopHyperbola(RotateMixedShape): + """A center column shield volume with a hyperbolic outer profile joined to + flat profiles at the top and bottom of the shield, and a constant + cylindrical inner profile. + + Args: + height (float): height of the center column shield. + arc_height (float): height of the outer hyperbolic profile of the + center column shield. + inner_radius (float): the inner radius of the center column shield. + mid_radius (float): the inner radius of the outer hyperbolic profile of + the center column shield. + outer_radius (float): the outer_radius of the center column shield. + stp_filename (str, optional): Defaults to + "CenterColumnShieldFlatTopHyperbola.stp". + stl_filename (str, optional): Defaults to + "CenterColumnShieldFlatTopHyperbola.stl". + name (str, optional): Defaults to "center_column". + material_tag (str, optional): Defaults to "center_column_shield_mat". + """ + + def __init__( + self, + height, + arc_height, + inner_radius, + mid_radius, + outer_radius, + stp_filename="CenterColumnShieldFlatTopHyperbola.stp", + stl_filename="CenterColumnShieldFlatTopHyperbola.stl", + name="center_column", + material_tag="center_column_shield_mat", + **kwargs + ): + + super().__init__( + name=name, + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + self.height = height + self.arc_height = arc_height + self.mid_radius = mid_radius + self.outer_radius = outer_radius + self.inner_radius = inner_radius + + @property + def height(self): + return self._height + + @height.setter + def height(self, height): + self._height = height + + @property + def arc_height(self): + return self._arc_height + + @arc_height.setter + def arc_height(self, arc_height): + self._arc_height = arc_height + + @property + def inner_radius(self): + return self._inner_radius + + @inner_radius.setter + def inner_radius(self, inner_radius): + self._inner_radius = inner_radius + + @property + def mid_radius(self): + return self._mid_radius + + @mid_radius.setter + def mid_radius(self, mid_radius): + self._mid_radius = mid_radius + + @property + def outer_radius(self): + return self._outer_radius + + @outer_radius.setter + def outer_radius(self, outer_radius): + self._outer_radius = outer_radius + + def find_points(self): + """Finds the XZ points and connection types (straight and spline) that + describe the 2D profile of the center column shield shape.""" + + if not self.inner_radius <= self.mid_radius <= self.outer_radius: + raise ValueError("inner_radius must be less than mid_radius. \ + mid_radius must be less than outer_radius.") + + if self.arc_height >= self.height: + raise ValueError( + "arc_height ({}) is larger than height ({})".format( + self.arc_height, self.height + ) + ) + + points = [ + (self.inner_radius, 0, "straight"), + (self.inner_radius, self.height / 2, "straight"), + (self.outer_radius, self.height / 2, "straight"), + (self.outer_radius, self.arc_height / 2, "spline"), + (self.mid_radius, 0, "spline"), + (self.outer_radius, -self.arc_height / 2, "straight"), + (self.outer_radius, -self.height / 2, "straight"), + (self.inner_radius, -self.height / 2, "straight") + ] + + self.points = points diff --git a/paramak/parametric_components/center_column_hyperbola.py b/paramak/parametric_components/center_column_hyperbola.py new file mode 100644 index 000000000..210536a74 --- /dev/null +++ b/paramak/parametric_components/center_column_hyperbola.py @@ -0,0 +1,96 @@ + +from paramak import RotateMixedShape + + +class CenterColumnShieldHyperbola(RotateMixedShape): + """A center column shield volume with a hyperbolic outer profile and + constant cylindrical inner profile. + + Args: + height (float): height of the center column shield. + inner_radius (float): the inner radius of the center column shield. + mid_radius (float): the inner radius of the outer hyperbolic profile of + the center column shield. + outer_radius (float): the outer radius of the center column shield. + stp_filename (str, optional): Defaults to "CenterColumnShieldHyperbola.stp". + stl_filename (str, optional): Defaults to "CenterColumnShieldHyperbola.stl". + name (str, optional): Defaults to "center_column". + material_tag (str, optional): Defaults to "center_column_shield_mat". + """ + + def __init__( + self, + height, + inner_radius, + mid_radius, + outer_radius, + stp_filename="CenterColumnShieldHyperbola.stp", + stl_filename="CenterColumnShieldHyperbola.stl", + name="center_column", + material_tag="center_column_shield_mat", + **kwargs + ): + + super().__init__( + name=name, + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + self.height = height + self.inner_radius = inner_radius + self.mid_radius = mid_radius + self.outer_radius = outer_radius + + @property + def height(self): + return self._height + + @height.setter + def height(self, height): + self._height = height + + @property + def inner_radius(self): + return self._inner_radius + + @inner_radius.setter + def inner_radius(self, inner_radius): + self._inner_radius = inner_radius + + @property + def mid_radius(self): + return self._mid_radius + + @mid_radius.setter + def mid_radius(self, mid_radius): + self._mid_radius = mid_radius + + @property + def outer_radius(self): + return self._outer_radius + + @outer_radius.setter + def outer_radius(self, outer_radius): + self._outer_radius = outer_radius + + def find_points(self): + """Finds the XZ points and connection types (straight and spline) that + describe the 2D profile of the center column shield shape.""" + + if not self.inner_radius <= self.mid_radius <= self.outer_radius: + raise ValueError("inner_radius must be less than mid radius. \ + mid_radius must be less than outer_radius.") + + points = [ + (self.inner_radius, 0, "straight"), + (self.inner_radius, self.height / 2, "straight"), + (self.outer_radius, self.height / 2, "spline"), + (self.mid_radius, 0, "spline"), + (self.outer_radius, -self.height / 2, "straight"), + (self.inner_radius, -self.height / 2, "straight") + ] + + self.points = points diff --git a/paramak/parametric_components/center_column_plasma_dependant.py b/paramak/parametric_components/center_column_plasma_dependant.py new file mode 100644 index 000000000..1b71de121 --- /dev/null +++ b/paramak/parametric_components/center_column_plasma_dependant.py @@ -0,0 +1,169 @@ + +from paramak import Plasma, RotateMixedShape + + +class CenterColumnShieldPlasmaHyperbola(RotateMixedShape): + """A center column shield volume with a curvature controlled by the shape + of the plasma and offsets specified at the plasma center and edges. Shield + thickness is controlled by the relative values of the shield offsets and + inner radius. + + Args: + height (float): height of the center column shield. + inner_radius (float): the inner radius of the center column shield. + mid_offset (float): the offset of the shield from the plasma at the + plasma center. + edge_offset (float): the offset of the shield from the plasma at the + plasma edge. + major_radius (int, optional): the major radius of the plasma. Defaults + to 450.0. + minor_radius (int, optional): the minor radius of the plasma. Defaults + to 150.0. + triangularity (float, optional): the triangularity of the plasma. + Defaults to 0.55. + elongation (float, optional): the elongation of the plasma. Defaults + to 2.0. + material_tag (str, optional): Defaults to "center_column_shield_mat". + stp_filename (str, optional): Defaults to + "CenterColumnShieldPlasmaHyperbola.stp". + stl_filename (str, optional): Defaults to + "CenterColumnShieldPlasmaHyperbola.stl". + """ + + def __init__( + self, + height, + inner_radius, + mid_offset, + edge_offset, + major_radius=450.0, + minor_radius=150.0, + triangularity=0.55, + elongation=2.0, + material_tag="center_column_shield_mat", + stp_filename="CenterColumnShieldPlasmaHyperbola.stp", + stl_filename="CenterColumnShieldPlasmaHyperbola.stl", + **kwargs + ): + + super().__init__( + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + self.major_radius = major_radius + self.minor_radius = minor_radius + self.triangularity = triangularity + self.elongation = elongation + self.stp_filename = stp_filename + self.height = height + self.inner_radius = inner_radius + self.mid_offset = mid_offset + self.edge_offset = edge_offset + + @property + def major_radius(self): + return self._major_radius + + @major_radius.setter + def major_radius(self, value): + self._major_radius = value + + @property + def minor_radius(self): + return self._minor_radius + + @minor_radius.setter + def minor_radius(self, value): + self._minor_radius = value + + @property + def triangularity(self): + return self._triangularity + + @triangularity.setter + def triangularity(self, value): + self._triangularity = value + + @property + def elongation(self): + return self._elongation + + @elongation.setter + def elongation(self, value): + self._elongation = value + + @property + def height(self): + return self._height + + @height.setter + def height(self, value): + self._height = value + + @property + def inner_radius(self): + return self._inner_radius + + @inner_radius.setter + def inner_radius(self, value): + self._inner_radius = value + + @property + def mid_offset(self): + return self._mid_offset + + @mid_offset.setter + def mid_offset(self, value): + self._mid_offset = value + + @property + def edge_offset(self): + return self._edge_offset + + @edge_offset.setter + def edge_offset(self, value): + self._edge_offset = value + + def find_points(self): + """Finds the XZ points and connection types (straight and spline) that + describe the 2D profile of the center column shield shape.""" + + plasma = Plasma() + + plasma.major_radius = self.major_radius + plasma.minor_radius = self.minor_radius + plasma.triangularity = self.triangularity + plasma.elongation = self.elongation + plasma.rotation_angle = self.rotation_angle + plasma.find_points() + + if self.height <= abs(plasma.high_point[1]) + abs(plasma.low_point[1]): + raise ValueError( + "Center column height ({}) is smaller than plasma height ({})".format( + self.height, abs(plasma.high_point[1]) + abs(plasma.low_point[1]) + ) + ) + + if self.inner_radius >= plasma.inner_equatorial_point[0] - \ + self.mid_offset: + raise ValueError("Inner radius is too large") + + points = [ + (self.inner_radius, 0, "straight"), + (self.inner_radius, self.height / 2, "straight"), + (plasma.high_point[0] - self.edge_offset, self.height / 2, "straight"), + (plasma.high_point[0] - self.edge_offset, plasma.high_point[1], "spline"), + ( + plasma.inner_equatorial_point[0] - self.mid_offset, + plasma.inner_equatorial_point[1], + "spline", + ), + (plasma.low_point[0] - self.edge_offset, plasma.low_point[1], "straight"), + (plasma.low_point[0] - self.edge_offset, -1 * self.height / 2, "straight"), + (self.inner_radius, -1 * self.height / 2, "straight") + ] + + self.points = points diff --git a/paramak/parametric_components/coolant_channel_ring_curved.py b/paramak/parametric_components/coolant_channel_ring_curved.py new file mode 100644 index 000000000..ee6339348 --- /dev/null +++ b/paramak/parametric_components/coolant_channel_ring_curved.py @@ -0,0 +1,103 @@ + +import numpy as np +from paramak import SweepCircleShape + + +class CoolantChannelRingCurved(SweepCircleShape): + """A ring of equally-spaced curved circular coolant channels with + constant thickness. + + Args: + height (float): height of each coolant channel in ring. + channel_radius (float): radius of each coolant channel in ring. + number_of_coolant_channels (float): number of coolant channels in ring. + ring_radius (float): radius of coolant channel ring. + workplane (str, optional): plane in which the cross-sections of the + coolant channels lie. Defaults to "XY". + start_angle (float, optional): angle at which the first channel in the + ring is placed. Defaults to 0. + path_workplane (str, optional): plane in which the cross-sections of + the coolant channels are swept. Defaults to "XZ". + rotation_axis (str, optional): azimuthal axis around which the separate + coolant channels are placed. Default calculated by workplane and + path_workplane. + force_cross_section (bool, optional): forces coolant channels to have a + more constant cross-section along their curve. Defaults to False. + stp_filename (str, optional): Defaults to + "CoolantChannelRingCurved.stp". + stl_filename (str, optional): Defaults to + "CoolantChannelRingCurved.stl". + material_tag (str, optional): Defaults to "coolant_channel_mat". + """ + + def __init__( + self, + height, + channel_radius, + number_of_coolant_channels, + ring_radius, + mid_offset, + start_angle=0, + stp_filename="CoolantChannelRingCurved.stp", + stl_filename="CoolantChannelRingCurved.stl", + material_tag="coolant_channel_mat", + **kwargs + ): + + self.ring_radius = ring_radius + self.mid_offset = mid_offset + self.height = height + self.start_angle = start_angle + + super().__init__( + path_points=self.path_points, + radius=channel_radius, + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + self.channel_radius = channel_radius + self.number_of_coolant_channels = number_of_coolant_channels + + @property + def azimuth_placement_angle(self): + self.find_azimuth_placement_angle() + return self._azimuth_placement_angle + + @azimuth_placement_angle.setter + def azimuth_placement_angle(self, value): + self._azimuth_placement_angle = value + + @property + def path_points(self): + self.find_path_points() + return self._path_points + + @path_points.setter + def path_points(self, value): + self._path_points = value + + def find_azimuth_placement_angle(self): + """Calculates the azimuth placement angles based on the number of + coolant channels.""" + + angles = list( + np.linspace( + 0 + self.start_angle, + 360 + self.start_angle, + self.number_of_coolant_channels, + endpoint=False)) + + self.azimuth_placement_angle = angles + + def find_path_points(self): + + path_points = [ + (self.ring_radius, -self.height / 2), + (self.ring_radius + self.mid_offset, 0), + (self.ring_radius, self.height / 2) + ] + + self.path_points = path_points diff --git a/paramak/parametric_components/coolant_channel_ring_straight.py b/paramak/parametric_components/coolant_channel_ring_straight.py new file mode 100644 index 000000000..c0540adef --- /dev/null +++ b/paramak/parametric_components/coolant_channel_ring_straight.py @@ -0,0 +1,82 @@ + +import numpy as np +from paramak import ExtrudeCircleShape + + +class CoolantChannelRingStraight(ExtrudeCircleShape): + """A ring of equally-spaced straight circular coolant channels with + constant thickness. + + Args: + height (float): height of each coolant channel in ring. + channel_radius (float): radius of each coolant channel in ring. + number_of_coolant_channels (float): number of coolant channels in ring. + ring radius (float): radius of coolant channel ring. + start_angle (float, optional): angle at which the first channel in the + ring is placed. Defaults to 0. + workplane (str, optional): plane in which the cross-sections of the + coolant channels lie. Defaults to "XY". + rotation_axis (str, optional): azimuthal axis around which the separate + coolant channels are placed. + stp_filename (str, optional): Defaults to + "CoolantChannelRingStraight.stp". + stl_filename (str, optional): Defaults to + "CoolantChannelRingStraight.stl". + material_tag (str, optional): Defaults to "coolant_channel_mat". + """ + + def __init__( + self, + height, + channel_radius, + number_of_coolant_channels, + ring_radius, + start_angle=0, + stp_filename="CoolantChannelRingStraight.stp", + stl_filename="CoolantChannelRingStraight.stl", + material_tag="coolant_channel_mat", + **kwargs + ): + + super().__init__( + distance=height, + radius=channel_radius, + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + self.height = height + self.channel_radius = channel_radius + self.number_of_coolant_channels = number_of_coolant_channels + self.ring_radius = ring_radius + self.start_angle = start_angle + + @property + def azimuth_placement_angle(self): + self.find_azimuth_placement_angle() + return self._azimuth_placement_angle + + @azimuth_placement_angle.setter + def azimuth_placement_angle(self, value): + self._azimuth_placement_angle = value + + def find_azimuth_placement_angle(self): + """Calculates the azimuth placement angles based on the number of + coolant channels.""" + + angles = list( + np.linspace( + 0 + self.start_angle, + 360 + self.start_angle, + self.number_of_coolant_channels, + endpoint=False)) + + self.azimuth_placement_angle = angles + + def find_points(self): + + points = [(self.ring_radius, 0)] + + self.points = points diff --git a/paramak/parametric_components/cutting_wedge.py b/paramak/parametric_components/cutting_wedge.py new file mode 100644 index 000000000..468350944 --- /dev/null +++ b/paramak/parametric_components/cutting_wedge.py @@ -0,0 +1,66 @@ + +from paramak import RotateStraightShape + + +class CuttingWedge(RotateStraightShape): + """Creates a wedge from height, radius and rotation angle arguments than + can be useful for cutting sector models. + + Args: + height (float): the vertical (z axis) height of the coil (cm). + radius (float): the horizontal (x axis) width of the coil (cm). + stp_filename (str, optional): Defaults to "CuttingWedge.stp". + stl_filename (str, optional): Defaults to "CuttingWedge.stl". + rotation_angle (float, optional): Defaults to 180.0. + material_tag (str, optional): Defaults to "cutting_slice_mat". + + """ + + def __init__( + self, + height, + radius, + stp_filename="CuttingWedge.stp", + stl_filename="CuttingWedge.stl", + rotation_angle=180.0, + material_tag="cutting_slice_mat", + **kwargs + ): + + super().__init__( + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + rotation_angle=rotation_angle, + **kwargs + ) + + self.height = height + self.radius = radius + + @property + def height(self): + return self._height + + @height.setter + def height(self, value): + self._height = value + + @property + def radius(self): + return self._radius + + @radius.setter + def radius(self, value): + self._radius = value + + def find_points(self): + + points = [ + (0, self.height / 2), + (self.radius, self.height / 2), + (self.radius, -self.height / 2), + (0, -self.height / 2) + ] + + self.points = points diff --git a/paramak/parametric_components/cutting_wedge_fs.py b/paramak/parametric_components/cutting_wedge_fs.py new file mode 100644 index 000000000..97a1fe730 --- /dev/null +++ b/paramak/parametric_components/cutting_wedge_fs.py @@ -0,0 +1,135 @@ + +from collections import Iterable +from operator import itemgetter + +from paramak import CuttingWedge + + +class CuttingWedgeFS(CuttingWedge): + """Creates a wedge from a Shape that can be useful for cutting sector + models. + + Args: + shape (paramak.Shape): a paramak.Shape object that is used to find the + height and radius of the wedge + stp_filename (str, optional): Defaults to "CuttingWedgeFS.stp". + stl_filename (str, optional): Defaults to "CuttingWedgeFS.stl". + material_tag (str, optional): Defaults to "cutting_slice_mat". + """ + + def __init__( + self, + shape, + stp_filename="CuttingWedgeAlternate.stp", + stl_filename="CuttingWedgeAlternate.stl", + material_tag="cutting_slice_mat", + **kwargs + ): + self.shape = shape + super().__init__( + height=self.height, + radius=self.radius, + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + @property + def shape(self): + return self._shape + + @shape.setter + def shape(self, value): + if value.rotation_angle == 360: + msg = 'cutting_wedge cannot be created,' + \ + ' rotation_angle must be < 360' + raise ValueError(msg) + self._shape = value + + @property + def radius(self): + self.find_radius_height() + return self._radius + + @radius.setter + def radius(self, value): + self._radius = value + + @property + def height(self): + self.find_radius_height() + return self._height + + @height.setter + def height(self, value): + self._height = value + + @property + def rotation_angle(self): + self.rotation_angle = 360 - self.shape.rotation_angle + return self._rotation_angle + + @rotation_angle.setter + def rotation_angle(self, value): + self._rotation_angle = value + + @property + def workplane(self): + workplanes = ["XY", "XZ", "YZ"] + for wp in workplanes: + if self.shape.get_rotation_axis()[1] in wp: + self.workplane = wp + break + return self._workplane + + @workplane.setter + def workplane(self, value): + self._workplane = value + + @property + def rotation_axis(self): + self.rotation_axis = self.shape.rotation_axis + return self._rotation_axis + + @rotation_axis.setter + def rotation_axis(self, value): + self._rotation_axis = value + + @property + def azimuth_placement_angle(self): + if isinstance(self.shape.azimuth_placement_angle, Iterable): + self.azimuth_placement_angle = self.shape.rotation_angle + else: + self.azimuth_placement_angle = \ + self.shape.azimuth_placement_angle + self.shape.rotation_angle + return self._azimuth_placement_angle + + @azimuth_placement_angle.setter + def azimuth_placement_angle(self, value): + self._azimuth_placement_angle = value + + def find_radius_height(self): + shape = self.shape + if shape.rotation_angle == 360: + msg = 'cutting_wedge cannot be created,' + \ + ' rotation_angle must be < 360' + raise ValueError(msg) + shape_points = shape.points + if hasattr(shape, 'radius') and len(shape_points) == 1: + max_x = shape_points[0][0] + shape.radius + max_y = shape_points[0][1] + shape.radius + + elif len(shape_points) > 1: + max_x = max(shape_points, key=itemgetter(0))[0] + if shape.get_rotation_axis()[1] not in shape.workplane and \ + hasattr(shape, "distance"): + max_y = shape.distance + else: + max_y = max(shape_points, key=itemgetter(1))[1] + + else: + raise ValueError('cutting_wedge cannot be created') + safety_factor = 3 + self.radius = safety_factor * max_x + self.height = safety_factor * max_y diff --git a/paramak/parametric_components/divertor_ITER.py b/paramak/parametric_components/divertor_ITER.py new file mode 100644 index 000000000..758d40d1c --- /dev/null +++ b/paramak/parametric_components/divertor_ITER.py @@ -0,0 +1,250 @@ + +import math + +import numpy as np +from paramak import (RotateMixedShape, distance_between_two_points, extend, + rotate) + + +class ITERtypeDivertor(RotateMixedShape): + """Creates an ITER-like divertor with inner and outer vertical targets and + dome + + Args: + anchors ((float, float), (float, float), optional): xz coordinates of + points at the top of vertical targets. + Defaults to ((450, -300), (561, -367)). + coverages ((float, float), optional): coverages (anticlockwise) in + degrees of the circular parts of vertical targets. + Defaults to (90, 180). + radii ((float, float), optional): radii (cm) of circular parts of the + vertical targets. Defaults to (50, 25). + lengths ((float, float), optional): leg length (cm) of the vertical + targets. Defaults to (78, 87). + dome (bool, optional): if set to False, the dome will not be created. + Defaults to True. + dome_height (float, optional): distance (cm) between the dome base and + lower points. Defaults to 43. + dome_length (float, optional): length of the dome. Defaults to 66. + dome_thickness (float, optional): thickness of the dome. + Defaults to 10. + dome_pos (float, optional): relative location of the dome between + vertical targets (0 inner, 1 outer). Ex: 0.5 will place the dome + in between the targets. Defaults to 0.5. + tilts ((float, float), optional): tilt angles (anticlockwise) in + degrees for the vertical targets. Defaults to (-27, 0). + stp_filename (str, optional): defaults to "ITERtypeDivertor.stp". + stl_filename (str, optional): defaults to "ITERtypeDivertor.stl". + """ + + def __init__( + self, + anchors=((450, -300), (561, -367)), + coverages=(90, 180), + radii=(50, 25), + lengths=(78, 87), + dome=True, + dome_height=43, + dome_length=66, + dome_thickness=10, + dome_pos=0.5, + tilts=(-27, 0), + stp_filename="ITERtypeDivertor.stp", + stl_filename="ITERtypeDivertor.stl", + **kwargs + ): + + super().__init__( + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + self.IVT_anchor, self.OVT_anchor = anchors + self.IVT_coverage, self.OVT_coverage = coverages + self.IVT_radius, self.OVT_radius = radii + self.IVT_length, self.OVT_length = lengths + self.IVT_tilt, self.OVT_tilt = tilts + self.dome = dome + self.dome_length = dome_length + self.dome_height = dome_height + self.dome_pos = dome_pos + self.dome_thickness = dome_thickness + + def _create_vertical_target_points( + self, anchor, coverage, tilt, radius, length): + """Creates a list of points for a vertical target + + Args: + anchor (float, float): xz coordinates of point at + the top of the vertical target. + coverage (float): coverages (anticlockwise) in degrees of the + circular part of the vertical target. + tilt (float): tilt angle (anticlockwise) in + degrees for the vertical target. + radius (float): radius (cm) of circular part of the vertical + target. + length (float): leg length (cm) of the vertical target. + + Returns: + list: list of x y coordinates + """ + points = [] + base_circle_inner = anchor[0] + radius, anchor[1] + A = rotate(base_circle_inner, anchor, coverage) + A_prime = rotate(base_circle_inner, anchor, coverage / 2) + C = (anchor[0], anchor[1] - length) + + A = rotate(anchor, A, tilt) + A_prime = rotate(anchor, A_prime, tilt) + C = rotate(anchor, C, tilt) + # upper inner A + points.append([A[0], A[1]]) + # A' + points.append([A_prime[0], A_prime[1]]) + # B + points.append([anchor[0], anchor[1]]) + # C + points.append([C[0], C[1]]) + return points + + def _create_dome_points( + self, C, F, dome_length, dome_height, dome_thickness, dome_pos + ): + """Creates a list of points for the dome alongside with their + connectivity + + Args: + C (float, float): coordinate of inner end of the dome + F (float, float): coordinate of outer end of the dome + dome_length (float): dome length (cm) + dome_height (float): dome height (cm) + dome_thickness (float): dome thickness (cm) + dome_pos (float): position of the dome between the two ends. + + Returns: + list: list of points with connectivity + ([[x, z, 'connection_type'], [...]]) + """ + points = [] + + dome_base = extend(C, F, dome_pos * distance_between_two_points(F, C)) + dome_lower_point = extend( + dome_base, rotate(dome_base, C, -math.pi / 2), dome_height + ) + + D_prime = extend( + dome_base, + dome_lower_point, + dome_height + + dome_thickness) + D = extend( + dome_lower_point, + rotate(dome_lower_point, D_prime, math.pi / 2), + dome_length / 2, + ) + E = extend( + dome_lower_point, + rotate(dome_lower_point, D_prime, -math.pi / 2), + dome_length / 2, + ) + + # D + points.append([D[0], D[1], "circle"]) + + # D' + points.append([D_prime[0], D_prime[1], "circle"]) + + # E + points.append([E[0], E[1], "straight"]) + return points + + def _create_casing_points(self, anchors, C, F, targets_lengths): + """Creates a list of points for the casing alongside with their + connectivity + + Args: + anchors ((float, float), (float, float)): xz coordinates of points + at the top of vertical targets. + C (float, float): coordinate of inner end of the dome + F (float, float): coordinate of outer end of the dome + targets_lengths (float, float): leg lengths of the vertical targets + + Returns: + list: list of points with connectivity + ([[x, z, 'connection_type'], [...]]) + """ + B, G = anchors + h1, h2 = targets_lengths + points = [] + # I + I_ = extend(C, F, distance_between_two_points(F, C) * 1.1) + points.append([I_[0], I_[1], "straight"]) + # J + J = extend(G, F, h2 * 1.2) + points.append([J[0], J[1], "straight"]) + # K + K = extend(B, C, h1 * 1.2) + points.append([K[0], K[1], "straight"]) + # L + L = extend(F, C, distance_between_two_points(F, C) * 1.1) + points.append([L[0], L[1], "straight"]) + return points + + def find_points(self): + """Finds the XZ points and connection types (straight and circle) that + describe the 2D profile of the ITER-like divertor + """ + + # IVT points + IVT_points = self._create_vertical_target_points( + self.IVT_anchor, + math.radians(self.IVT_coverage), + math.radians(self.IVT_tilt), + -self.IVT_radius, + self.IVT_length, + ) + # add connections + connections = ["circle"] * 2 + ["straight"] * 2 + for i, connection in enumerate(connections): + IVT_points[i].append(connection) + + # OVT points + OVT_points = self._create_vertical_target_points( + self.OVT_anchor, + -math.radians(self.OVT_coverage), + math.radians(self.OVT_tilt), + self.OVT_radius, + self.OVT_length, + ) + # add connections + connections = ["straight"] + ["circle"] * 2 + ["straight"] + for i, connection in enumerate(connections): + OVT_points[i].append(connection) + # OVT_points need to be fliped for correct connections + OVT_points = [[float(e[0]), float(e[1]), e[2]] + for e in np.flipud(OVT_points)] + + # Dome points + dome_points = [] + if self.dome: + dome_points = self._create_dome_points( + IVT_points[-1][:2], + OVT_points[0][:2], + self.dome_length, + self.dome_height, + self.dome_thickness, + self.dome_pos, + ) + + # casing points + casing_points = self._create_casing_points( + anchors=(self.IVT_anchor, self.OVT_anchor), + C=IVT_points[-1][:2], + F=OVT_points[0][:2], + targets_lengths=(self.IVT_length, self.OVT_length), + ) + + # append all points + points = IVT_points + dome_points + OVT_points + casing_points + self.points = points diff --git a/paramak/parametric_components/divertor_ITER_no_dome.py b/paramak/parametric_components/divertor_ITER_no_dome.py new file mode 100644 index 000000000..77d45f884 --- /dev/null +++ b/paramak/parametric_components/divertor_ITER_no_dome.py @@ -0,0 +1,21 @@ + +from paramak import ITERtypeDivertor + + +class ITERtypeDivertorNoDome(ITERtypeDivertor): + """Creates an ITER-like divertor with inner and outer vertical targets + """ + + def __init__( + self, + **kwargs + ): + + super().__init__( + dome=False, + dome_height=None, + dome_length=None, + dome_thickness=None, + dome_pos=None, + **kwargs + ) diff --git a/paramak/parametric_components/hollow_cube.py b/paramak/parametric_components/hollow_cube.py new file mode 100644 index 000000000..036c35732 --- /dev/null +++ b/paramak/parametric_components/hollow_cube.py @@ -0,0 +1,74 @@ + +import cadquery as cq +from paramak import Shape + + +class HollowCube(Shape): + """A hollow cube with a constant thickness. Can be used to create a DAGMC + Graveyard. + + Arguments: + length (float): The length to use for the height, width, depth of the + inner dimentions of the cube. + thickness (float, optional): thickness of the vessel. Defaults to 10.0. + stp_filename (str, optional): Defaults to "HollowCube.stp". + stl_filename (str, optional): Defaults to "HollowCube.stl". + material_tag (str, optional): defaults to "hollow_cube_mat". + """ + + def __init__( + self, + length, + thickness=10., + stp_filename="HollowCube.stp", + stl_filename="HollowCube.stl", + material_tag="hollow_cube_mat", + **kwargs + ): + self.length = length + self.thickness = thickness + super().__init__( + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + @property + def thickness(self): + return self._thickness + + @thickness.setter + def thickness(self, value): + self._thickness = value + + @property + def length(self): + return self._length + + @length.setter + def length(self, value): + self._length = value + + def create_solid(self): + + # creates a small box that surrounds the geometry + inner_box = cq.Workplane("front").box( + self.length, + self.length, + self.length + ) + + # creates a large box that surrounds the smaller box + outer_box = cq.Workplane("front").box( + self.length + self.thickness, + self.length + self.thickness, + self.length + self.thickness + ) + + # subtracts the two boxes to leave a hollow box + new_shape = outer_box.cut(inner_box) + + self.solid = new_shape + + return new_shape diff --git a/paramak/parametric_components/inboard_firstwall_fccs.py b/paramak/parametric_components/inboard_firstwall_fccs.py new file mode 100644 index 000000000..c80cf1834 --- /dev/null +++ b/paramak/parametric_components/inboard_firstwall_fccs.py @@ -0,0 +1,161 @@ + +from collections import Iterable + +from paramak import (CenterColumnShieldCircular, CenterColumnShieldCylinder, + CenterColumnShieldFlatTopCircular, + CenterColumnShieldFlatTopHyperbola, + CenterColumnShieldHyperbola, + CenterColumnShieldPlasmaHyperbola, RotateMixedShape) + + +class InboardFirstwallFCCS(RotateMixedShape): + """An inboard firstwall component that builds a constant thickness layer + from the central column shield. The center column shields can be of type: + CenterColumnShieldCylinder, CenterColumnShieldHyperbola, + CenterColumnShieldFlatTopHyperbola, CenterColumnShieldCircular, + CenterColumnShieldPlasmaHyperbola or CenterColumnShieldFlatTopCircular + + Args: + central_column_shield (paramak.Shape): The central column shield object + to build from + thickness (float): the radial thickness of the firstwall (cm) + stp_filename (str, optional): Defaults to "InboardFirstwallFCCS.stp". + stl_filename (str, optional): Defaults to "InboardFirstwallFCCS.stl". + material_tag (str, optional): Defaults to "firstwall_mat". + """ + + def __init__( + self, + central_column_shield, + thickness, + stp_filename="InboardFirstwallFCCS.stp", + stl_filename="InboardFirstwallFCCS.stl", + material_tag="firstwall_mat", + **kwargs + ): + + super().__init__( + stp_filename=stp_filename, + stl_filename=stl_filename, + material_tag=material_tag, + **kwargs + ) + + self.central_column_shield = central_column_shield + self.thickness = thickness + + @property + def thickness(self): + return self._thickness + + @thickness.setter + def thickness(self, value): + self._thickness = value + + @property + def central_column_shield(self): + return self._central_column_shield + + @central_column_shield.setter + def central_column_shield(self, value): + self._central_column_shield = value + + def find_points(self): + + # check that is an acceptable class + acceptable_classes = ( + CenterColumnShieldCylinder, + CenterColumnShieldHyperbola, + CenterColumnShieldFlatTopHyperbola, + CenterColumnShieldPlasmaHyperbola, + CenterColumnShieldCircular, + CenterColumnShieldFlatTopCircular + ) + if not isinstance(self.central_column_shield, acceptable_classes): + raise ValueError( + "InboardFirstwallFCCS.central_column_shield must be an \ + instance of CenterColumnShieldCylinder, \ + CenterColumnShieldHyperbola, \ + CenterColumnShieldFlatTopHyperbola, \ + CenterColumnShieldPlasmaHyperbola, \ + CenterColumnShieldCircular, CenterColumnShieldFlatTopCircular") + + inner_radius = self.central_column_shield.inner_radius + height = self.central_column_shield.height + + if isinstance(self.central_column_shield, CenterColumnShieldCylinder): + firstwall = CenterColumnShieldCylinder( + height=height, + inner_radius=inner_radius, + outer_radius=self.central_column_shield.outer_radius + + self.thickness + ) + + elif isinstance(self.central_column_shield, + CenterColumnShieldHyperbola): + firstwall = CenterColumnShieldHyperbola( + height=height, + inner_radius=inner_radius, + mid_radius=self.central_column_shield.mid_radius + + self.thickness, + outer_radius=self.central_column_shield.outer_radius + + self.thickness, + ) + + elif isinstance(self.central_column_shield, + CenterColumnShieldFlatTopHyperbola): + firstwall = CenterColumnShieldFlatTopHyperbola( + height=height, + arc_height=self.central_column_shield.arc_height, + inner_radius=inner_radius, + mid_radius=self.central_column_shield.mid_radius + + self.thickness, + outer_radius=self.central_column_shield.outer_radius + + self.thickness, + ) + + elif isinstance(self.central_column_shield, + CenterColumnShieldPlasmaHyperbola): + firstwall = CenterColumnShieldPlasmaHyperbola( + height=height, + inner_radius=inner_radius, + mid_offset=self.central_column_shield.mid_offset - + self.thickness, + edge_offset=self.central_column_shield.edge_offset - + self.thickness, + ) + + elif isinstance(self.central_column_shield, + CenterColumnShieldCircular): + firstwall = CenterColumnShieldCircular( + height=height, + inner_radius=inner_radius, + mid_radius=self.central_column_shield.mid_radius + + self.thickness, + outer_radius=self.central_column_shield.outer_radius + + self.thickness, + ) + + elif isinstance(self.central_column_shield, + CenterColumnShieldFlatTopCircular): + firstwall = CenterColumnShieldFlatTopCircular( + height=height, + arc_height=self.central_column_shield.arc_height, + inner_radius=inner_radius, + mid_radius=self.central_column_shield.mid_radius + + self.thickness, + outer_radius=self.central_column_shield.outer_radius + + self.thickness, + ) + + firstwall.rotation_angle = self.rotation_angle + points = firstwall.points[:-1] # remove last point + self.points = points + + # add to cut attribute + if self.cut is None: + self.cut = self.central_column_shield + elif isinstance(self.cut, Iterable): + self.cut = [*self.cut, self.central_column_shield] + else: + self.cut = [*[self.cut], self.central_column_shield] diff --git a/paramak/parametric_components/inner_tf_coils_circular.py b/paramak/parametric_components/inner_tf_coils_circular.py new file mode 100644 index 000000000..e80f202ed --- /dev/null +++ b/paramak/parametric_components/inner_tf_coils_circular.py @@ -0,0 +1,214 @@ + +import math + +import numpy as np +from paramak import ExtrudeMixedShape + + +class InnerTfCoilsCircular(ExtrudeMixedShape): + """A tf coil volume with cylindrical inner and outer profiles and + constant gaps between each coil. + + Args: + height (float): height of tf coils. + inner_radius (float): inner radius of tf coils. + outer_radius (float): outer radius of tf coils. + number_of_coils (int): number of tf coils. + gap_size (float): gap between adjacent tf coils. + azimuth_start_angle (float, optional): Defaults to 0.0. + stp_filename (str, optional): Defaults to "InnerTfCoilsCircular.stp". + stl_filename (str, optional): Defaults to "InnerTfCoilsCircular.stl". + material_tag (str, optional): Defaults to "inner_tf_coil_mat". + workplane (str, optional): Defaults to "XY". + rotation_axis (str, optional): Defaults to "Z". + """ + + def __init__( + self, + height, + inner_radius, + outer_radius, + number_of_coils, + gap_size, + azimuth_start_angle=0.0, + stp_filename="InnerTfCoilsCircular.stp", + stl_filename="InnerTfCoilsCircular.stl", + material_tag="inner_tf_coil_mat", + workplane="XY", + rotation_axis="Z", + **kwargs + ): + + super().__init__( + distance=height, + stp_filename=stp_filename, + stl_filename=stl_filename, + material_tag=material_tag, + workplane=workplane, + rotation_axis=rotation_axis, + **kwargs + ) + + self.azimuth_start_angle = azimuth_start_angle + self.height = height + self.inner_radius = inner_radius + self.outer_radius = outer_radius + self.number_of_coils = number_of_coils + self.gap_size = gap_size + self.distance = height + + @property + def azimuth_start_angle(self): + return self._azimuth_start_angle + + @azimuth_start_angle.setter + def azimuth_start_angle(self, value): + self._azimuth_start_angle = value + + @property + def azimuth_placement_angle(self): + self.find_azimuth_placement_angle() + return self._azimuth_placement_angle + + @azimuth_placement_angle.setter + def azimuth_placement_angle(self, value): + self._azimuth_placement_angle = value + + @property + def height(self): + return self._height + + @height.setter + def height(self, height): + self._height = height + + @property + def distance(self): + return self.height + + @distance.setter + def distance(self, value): + self._distance = value + + @property + def inner_radius(self): + return self._inner_radius + + @inner_radius.setter + def inner_radius(self, inner_radius): + self._inner_radius = inner_radius + + @property + def outer_radius(self): + return self._outer_radius + + @outer_radius.setter + def outer_radius(self, outer_radius): + self._outer_radius = outer_radius + + @property + def number_of_coils(self): + return self._number_of_coils + + @number_of_coils.setter + def number_of_coils(self, number_of_coils): + self._number_of_coils = number_of_coils + + @property + def gap_size(self): + return self._gap_size + + @gap_size.setter + def gap_size(self, gap_size): + self._gap_size = gap_size + + def find_points(self): + """Finds the points that describe the 2D profile of the tf coil shape""" + + if self.gap_size * self.number_of_coils > 2 * math.pi * self.inner_radius: + raise ValueError('gap_size is too large') + + theta_inner = ( + (2 * math.pi * self.inner_radius) - (self.gap_size * self.number_of_coils) + ) / (self.inner_radius * self.number_of_coils) + omega_inner = math.asin(self.gap_size / (2 * self.inner_radius)) + + theta_outer = ( + (2 * math.pi * self.outer_radius) - (self.gap_size * self.number_of_coils) + ) / (self.outer_radius * self.number_of_coils) + omega_outer = math.asin(self.gap_size / (2 * self.outer_radius)) + + # inner points + point_1 = ( + (self.inner_radius * math.cos(-omega_inner)), + (-self.inner_radius * math.sin(-omega_inner)), + ) + point_2 = ( + ( + self.inner_radius * math.cos(theta_inner / 2) * math.cos(-omega_inner) + + self.inner_radius * math.sin(theta_inner / 2) * math.sin(-omega_inner) + ), + ( + -self.inner_radius * math.cos(theta_inner / 2) * math.sin(-omega_inner) + + self.inner_radius * math.sin(theta_inner / 2) * math.cos(-omega_inner) + ), + ) + point_3 = ( + ( + self.inner_radius * math.cos(theta_inner) * math.cos(-omega_inner) + + self.inner_radius * math.sin(theta_inner) * math.sin(-omega_inner) + ), + ( + -self.inner_radius * math.cos(theta_inner) * math.sin(-omega_inner) + + self.inner_radius * math.sin(theta_inner) * math.cos(-omega_inner) + ), + ) + + # outer points + point_4 = ( + (self.outer_radius * math.cos(-omega_outer)), + (-self.outer_radius * math.sin(-omega_outer)), + ) + point_5 = ( + ( + self.outer_radius * math.cos(theta_outer / 2) * math.cos(-omega_outer) + + self.outer_radius * math.sin(theta_outer / 2) * math.sin(-omega_outer) + ), + ( + -self.outer_radius * math.cos(theta_outer / 2) * math.sin(-omega_outer) + + self.outer_radius * math.sin(theta_outer / 2) * math.cos(-omega_outer) + ), + ) + point_6 = ( + ( + self.outer_radius * math.cos(theta_outer) * math.cos(-omega_outer) + + self.outer_radius * math.sin(theta_outer) * math.sin(-omega_outer) + ), + ( + -self.outer_radius * math.cos(theta_outer) * math.sin(-omega_outer) + + self.outer_radius * math.sin(theta_outer) * math.cos(-omega_outer) + ), + ) + + points = [ + (point_1[0], point_1[1], "circle"), + (point_2[0], point_2[1], "circle"), + (point_3[0], point_3[1], "straight"), + (point_6[0], point_6[1], "circle"), + (point_5[0], point_5[1], "circle"), + (point_4[0], point_4[1], "straight"), + ] + + self.points = points + + def find_azimuth_placement_angle(self): + """Calculates the azimuth placement angles based on the number of tf coils""" + + angles = list( + np.linspace( + 0 + self.azimuth_start_angle, + 360 + self.azimuth_start_angle, + self.number_of_coils, + endpoint=False)) + + self.azimuth_placement_angle = angles diff --git a/paramak/parametric_components/inner_tf_coils_flat.py b/paramak/parametric_components/inner_tf_coils_flat.py new file mode 100644 index 000000000..e3de68c14 --- /dev/null +++ b/paramak/parametric_components/inner_tf_coils_flat.py @@ -0,0 +1,193 @@ + +import math + +import numpy as np +from paramak import ExtrudeStraightShape + + +class InnerTfCoilsFlat(ExtrudeStraightShape): + """A tf coil volume with straight inner and outer profiles and + constant gaps between each coil. + + Args: + height (float): height of tf coils. + inner_radius (float): inner radius of tf coils. + outer_radius (float): outer radius of tf coils. + number_of_coils (int): number of tf coils. + gap_size (float): gap between adjacent tf coils. + azimuth_start_angle (float, optional): defaults to 0.0. + stp_filename (str, optional): defaults to "InnerTfCoilsFlat.stp". + stl_filename (str, optional): defaults to "InnerTfCoilsFlat.stl". + material_tag (str, optional): defaults to "inner_tf_coil_mat". + workplane (str, optional):defaults to "XY". + rotation_axis (str, optional): Defaults to "Z". + """ + + def __init__( + self, + height, + inner_radius, + outer_radius, + number_of_coils, + gap_size, + azimuth_start_angle=0.0, + stp_filename="InnerTfCoilsFlat.stp", + stl_filename="InnerTfCoilsFlat.stl", + material_tag="inner_tf_coil_mat", + workplane="XY", + rotation_axis="Z", + **kwargs + ): + + super().__init__( + distance=height, + stp_filename=stp_filename, + stl_filename=stl_filename, + material_tag=material_tag, + workplane=workplane, + rotation_axis=rotation_axis, + **kwargs + ) + + self.azimuth_start_angle = azimuth_start_angle + self.height = height + self.inner_radius = inner_radius + self.outer_radius = outer_radius + self.number_of_coils = number_of_coils + self.gap_size = gap_size + self.distance = height + + @property + def azimuth_start_angle(self): + return self._azimuth_start_angle + + @azimuth_start_angle.setter + def azimuth_start_angle(self, value): + self._azimuth_start_angle = value + + @property + def azimuth_placement_angle(self): + self.find_azimuth_placement_angle() + return self._azimuth_placement_angle + + @azimuth_placement_angle.setter + def azimuth_placement_angle(self, value): + self._azimuth_placement_angle = value + + @property + def height(self): + return self._height + + @height.setter + def height(self, height): + self._height = height + + @property + def distance(self): + return self.height + + @distance.setter + def distance(self, value): + self._distance = value + + @property + def inner_radius(self): + return self._inner_radius + + @inner_radius.setter + def inner_radius(self, inner_radius): + self._inner_radius = inner_radius + + @property + def outer_radius(self): + return self._outer_radius + + @outer_radius.setter + def outer_radius(self, outer_radius): + self._outer_radius = outer_radius + + @property + def number_of_coils(self): + return self._number_of_coils + + @number_of_coils.setter + def number_of_coils(self, number_of_coils): + self._number_of_coils = number_of_coils + + @property + def gap_size(self): + return self._gap_size + + @gap_size.setter + def gap_size(self, gap_size): + self._gap_size = gap_size + + def find_points(self): + """Finds the points that describe the 2D profile of the tf coil shape""" + + if self.gap_size * self.number_of_coils > 2 * math.pi * self.inner_radius: + raise ValueError('gap_size is too large') + + theta_inner = ( + (2 * math.pi * self.inner_radius) - (self.gap_size * self.number_of_coils) + ) / (self.inner_radius * self.number_of_coils) + omega_inner = math.asin(self.gap_size / (2 * self.inner_radius)) + + theta_outer = ( + (2 * math.pi * self.outer_radius) - (self.gap_size * self.number_of_coils) + ) / (self.outer_radius * self.number_of_coils) + omega_outer = math.asin(self.gap_size / (2 * self.outer_radius)) + + # inner points + point_1 = ( + (self.inner_radius * math.cos(-omega_inner)), + (-self.inner_radius * math.sin(-omega_inner)), + ) + point_3 = ( + ( + self.inner_radius * math.cos(theta_inner) * math.cos(-omega_inner) + + self.inner_radius * math.sin(theta_inner) * math.sin(-omega_inner) + ), + ( + -self.inner_radius * math.cos(theta_inner) * math.sin(-omega_inner) + + self.inner_radius * math.sin(theta_inner) * math.cos(-omega_inner) + ), + ) + + # outer points + point_4 = ( + (self.outer_radius * math.cos(-omega_outer)), + (-self.outer_radius * math.sin(-omega_outer)), + ) + point_6 = ( + ( + self.outer_radius * math.cos(theta_outer) * math.cos(-omega_outer) + + self.outer_radius * math.sin(theta_outer) * math.sin(-omega_outer) + ), + ( + -self.outer_radius * math.cos(theta_outer) * math.sin(-omega_outer) + + self.outer_radius * math.sin(theta_outer) * math.cos(-omega_outer) + ), + ) + + points = [ + (point_1[0], point_1[1]), + (point_3[0], point_3[1]), + (point_6[0], point_6[1]), + (point_4[0], point_4[1]), + ] + + self.points = points + + def find_azimuth_placement_angle(self): + """Calculates the azimuth placement angles based on the number of tf + coils""" + + angles = list( + np.linspace( + 0 + self.azimuth_start_angle, + 360 + self.azimuth_start_angle, + self.number_of_coils, + endpoint=False)) + + self.azimuth_placement_angle = angles diff --git a/paramak/parametric_components/poloidal_field_coil.py b/paramak/parametric_components/poloidal_field_coil.py new file mode 100644 index 000000000..54626b37c --- /dev/null +++ b/paramak/parametric_components/poloidal_field_coil.py @@ -0,0 +1,90 @@ + +from paramak import RotateStraightShape + + +class PoloidalFieldCoil(RotateStraightShape): + """Creates a rectangular poloidal field coil. + + Args: + height (float): the vertical (z axis) height of the coil (cm). + width (float): the horizontal (x axis) width of the coil (cm). + center_point (tuple of floats): the center of the coil (x,z) values + (cm). + stp_filename (str, optional): defaults to "PoloidalFieldCoil.stp". + stl_filename (str, optional): defaults to "PoloidalFieldCoil.stl". + name (str, optional): defaults to "pf_coil". + material_tag (str, optional): defaults to "pf_coil_mat". + """ + + def __init__( + self, + height, + width, + center_point, + stp_filename="PoloidalFieldCoil.stp", + stl_filename="PoloidalFieldCoil.stl", + name="pf_coil", + material_tag="pf_coil_mat", + **kwargs + ): + + super().__init__( + name=name, + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + self.center_point = center_point + self.height = height + self.width = width + + @property + def center_point(self): + return self._center_point + + @center_point.setter + def center_point(self, center_point): + self._center_point = center_point + + @property + def height(self): + return self._height + + @height.setter + def height(self, height): + self._height = height + + @property + def width(self): + return self._width + + @width.setter + def width(self, width): + self._width = width + + def find_points(self): + """Finds the XZ points joined by straight connections that describe + the 2D profile of the poloidal field coil shape.""" + + points = [ + ( + self.center_point[0] + self.width / 2.0, + self.center_point[1] + self.height / 2.0, + ), # upper right + ( + self.center_point[0] + self.width / 2.0, + self.center_point[1] - self.height / 2.0, + ), # lower right + ( + self.center_point[0] - self.width / 2.0, + self.center_point[1] - self.height / 2.0, + ), # lower left + ( + self.center_point[0] - self.width / 2.0, + self.center_point[1] + self.height / 2.0, + ) + ] + + self.points = points diff --git a/paramak/parametric_components/poloidal_field_coil_case.py b/paramak/parametric_components/poloidal_field_coil_case.py new file mode 100644 index 000000000..51af0e9aa --- /dev/null +++ b/paramak/parametric_components/poloidal_field_coil_case.py @@ -0,0 +1,126 @@ + +from paramak import RotateStraightShape + + +class PoloidalFieldCoilCase(RotateStraightShape): + """Creates a casing for a rectangular coil from inputs that + describe the existing coil and the thickness of the casing required. + + Args: + coil_height (float): the vertical (z axis) height of the coil (cm). + coil_width (float): the horizontal(x axis) width of the coil (cm). + center_point (tuple of floats): the center of the coil (x,z) values + (cm). + casing_thickness (tuple of floats): the thickness of the coil casing + (cm). + stp_filename (str, optional): defaults to "PoloidalFieldCoilCase.stp". + stl_filename (str, optional): defaults to "PoloidalFieldCoilCase.stl". + material_tag (str, optional): defaults to "pf_coil_case_mat". + """ + + def __init__( + self, + casing_thickness, + coil_height, + coil_width, + center_point, + stp_filename="PoloidalFieldCoilCase.stp", + stl_filename="PoloidalFieldCoilCase.stl", + material_tag="pf_coil_case_mat", + **kwargs + ): + + super().__init__( + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + self.center_point = center_point + self.height = coil_height + self.width = coil_width + self.casing_thickness = casing_thickness + + @property + def center_point(self): + return self._center_point + + @center_point.setter + def center_point(self, value): + self._center_point = value + + @property + def height(self): + return self._height + + @height.setter + def height(self, height): + self._height = height + + @property + def width(self): + return self._width + + @width.setter + def width(self, width): + self._width = width + + def find_points(self): + """Finds the XZ points joined by straight connections that describe + the 2D profile of the poloidal field coil case shape.""" + + points = [ + ( + self.center_point[0] + self.width / 2.0, + self.center_point[1] + self.height / 2.0, + ), # upper right + ( + self.center_point[0] + self.width / 2.0, + self.center_point[1] - self.height / 2.0, + ), # lower right + ( + self.center_point[0] - self.width / 2.0, + self.center_point[1] - self.height / 2.0, + ), # lower left + ( + self.center_point[0] - self.width / 2.0, + self.center_point[1] + self.height / 2.0, + ), # upper left + ( + self.center_point[0] + self.width / 2.0, + self.center_point[1] + self.height / 2.0, + ), # upper right + ( + self.center_point[0] + \ + (self.casing_thickness + self.width / 2.0), + self.center_point[1] + \ + (self.casing_thickness + self.height / 2.0), + ), + ( + self.center_point[0] + \ + (self.casing_thickness + self.width / 2.0), + self.center_point[1] - \ + (self.casing_thickness + self.height / 2.0), + ), + ( + self.center_point[0] - \ + (self.casing_thickness + self.width / 2.0), + self.center_point[1] - \ + (self.casing_thickness + self.height / 2.0), + ), + ( + self.center_point[0] - \ + (self.casing_thickness + self.width / 2.0), + self.center_point[1] + \ + (self.casing_thickness + self.height / 2.0), + ), + ( + self.center_point[0] + \ + (self.casing_thickness + self.width / 2.0), + self.center_point[1] + \ + (self.casing_thickness + self.height / 2.0), + ) + ] + + self.points = points diff --git a/paramak/parametric_components/poloidal_field_coil_case_fc.py b/paramak/parametric_components/poloidal_field_coil_case_fc.py new file mode 100644 index 000000000..8563744e7 --- /dev/null +++ b/paramak/parametric_components/poloidal_field_coil_case_fc.py @@ -0,0 +1,123 @@ + +from paramak import RotateStraightShape + + +class PoloidalFieldCoilCaseFC(RotateStraightShape): + """Creates a casing for a rectangular poloidal field coil by building + around an existing coil (which is passed as an argument on construction). + + Args: + pf_coil (paramak.PoloidalFieldCoil): a pf coil object with a set width, + height and center point. + casing_thickness (float): the thickness of the coil casing (cm). + stp_filename (str, optional): defaults to + "PoloidalFieldCoilCaseFC.stp". + stl_filename (str, optional): defaults to + "PoloidalFieldCoilCaseFC.stl". + material_tag (str, optional): defaults to "pf_coil_case_mat". + """ + + def __init__( + self, + pf_coil, + casing_thickness, + stp_filename="PoloidalFieldCoilCaseFC.stp", + stl_filename="PoloidalFieldCoilCaseFC.stl", + material_tag="pf_coil_case_mat", + **kwargs + ): + + super().__init__( + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + self.center_point = pf_coil.center_point + self.height = pf_coil.height + self.width = pf_coil.width + self.casing_thickness = casing_thickness + + @property + def center_point(self): + return self._center_point + + @center_point.setter + def center_point(self, value): + self._center_point = value + + @property + def height(self): + return self._height + + @height.setter + def height(self, height): + self._height = height + + @property + def width(self): + return self._width + + @width.setter + def width(self, width): + self._width = width + + def find_points(self): + """Finds the XZ points joined by straight connections that describe + the 2D profile of the poloidal field coil case shape.""" + + points = [ + ( + self.center_point[0] + self.width / 2.0, + self.center_point[1] + self.height / 2.0, + ), # upper right + ( + self.center_point[0] + self.width / 2.0, + self.center_point[1] - self.height / 2.0, + ), # lower right + ( + self.center_point[0] - self.width / 2.0, + self.center_point[1] - self.height / 2.0, + ), # lower left + ( + self.center_point[0] - self.width / 2.0, + self.center_point[1] + self.height / 2.0, + ), # upper left + ( + self.center_point[0] + self.width / 2.0, + self.center_point[1] + self.height / 2.0, + ), # upper right + ( + self.center_point[0] + \ + (self.casing_thickness + self.width / 2.0), + self.center_point[1] + \ + (self.casing_thickness + self.height / 2.0), + ), + ( + self.center_point[0] + \ + (self.casing_thickness + self.width / 2.0), + self.center_point[1] - \ + (self.casing_thickness + self.height / 2.0), + ), + ( + self.center_point[0] - \ + (self.casing_thickness + self.width / 2.0), + self.center_point[1] - \ + (self.casing_thickness + self.height / 2.0), + ), + ( + self.center_point[0] - \ + (self.casing_thickness + self.width / 2.0), + self.center_point[1] + \ + (self.casing_thickness + self.height / 2.0), + ), + ( + self.center_point[0] + \ + (self.casing_thickness + self.width / 2.0), + self.center_point[1] + \ + (self.casing_thickness + self.height / 2.0), + ) + ] + + self.points = points diff --git a/paramak/parametric_components/poloidal_field_coil_case_set.py b/paramak/parametric_components/poloidal_field_coil_case_set.py new file mode 100644 index 000000000..0d52202a5 --- /dev/null +++ b/paramak/parametric_components/poloidal_field_coil_case_set.py @@ -0,0 +1,194 @@ + +import cadquery as cq +from paramak import RotateStraightShape + + +class PoloidalFieldCoilCaseSet(RotateStraightShape): + """Creates a series of rectangular poloidal field coils. + + Args: + heights (list of floats): the vertical (z axis) heights of the coil + (cm). + widths (list of floats): the horizontal (x axis) widths of the coil + (cm). + casing_thicknesses (float or list of floats): the thicknesses of the + coil casing (cm). If float then the same thickness is applied to + all coils. If list of floats then each entry is applied to a + seperate pf_coil, one entry for each pf_coil. + center_points (tuple of floats): the center of the coil (x,z) values + (cm). + stp_filename (str, optional): defaults to "PoloidalFieldCoilCaseSet.stp". + stl_filename (str, optional): defaults to "PoloidalFieldCoilCaseSet.stl". + name (str, optional): defaults to "pf_coil_case_set". + material_tag (str, optional): defaults to "pf_coil_case_mat". + """ + + def __init__( + self, + heights, + widths, + casing_thicknesses, + center_points, + stp_filename="PoloidalFieldCoilCaseSet.stp", + stl_filename="PoloidalFieldCoilCaseSet.stl", + name="pf_coil_case_set", + material_tag="pf_coil_case_mat", + **kwargs + ): + + super().__init__( + name=name, + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + self.center_points = center_points + self.heights = heights + self.widths = widths + self.casing_thicknesses = casing_thicknesses + + @property + def center_points(self): + return self._center_points + + @center_points.setter + def center_points(self, center_points): + self._center_points = center_points + + @property + def heights(self): + return self._heights + + @heights.setter + def heights(self, heights): + self._heights = heights + + @property + def widths(self): + return self._widths + + @widths.setter + def widths(self, widths): + self._widths = widths + + @property + def casing_thicknesses(self): + return self._casing_thicknesses + + @casing_thicknesses.setter + def casing_thicknesses(self, value): + if isinstance(value, list): + if not all(isinstance(x, (int, float)) for x in value): + raise ValueError( + "Every entry in Casing_thicknesses must be a float or int") + else: + if not isinstance(value, (float, int)): + raise ValueError( + "Casing_thicknesses must be a list of numbers or a number") + self._casing_thicknesses = value + + def find_points(self): + """Finds the XZ points joined by straight connections that describe + the 2D profile of the poloidal field coil shape.""" + + all_points = [] + + if isinstance(self.casing_thicknesses, list): + casing_thicknesses_list = self.casing_thicknesses + else: + casing_thicknesses_list = [ + self.casing_thicknesses] * len(self.widths) + + if not len( + self.heights) == len( + self.widths) == len( + self.center_points) == len(casing_thicknesses_list): + raise ValueError( + "The number of heights, widths, center_points and " + "casing_thicknesses must be equal") + + for height, width, center_point, casing_thickness in zip( + self.heights, self.widths, + self.center_points, casing_thicknesses_list): + + if casing_thickness != 0: + + all_points = all_points + [ + ( + center_point[0] + width / 2.0, + center_point[1] + height / 2.0, + ), # upper right + ( + center_point[0] + width / 2.0, + center_point[1] - height / 2.0, + ), # lower right + ( + center_point[0] - width / 2.0, + center_point[1] - height / 2.0, + ), # lower left + ( + center_point[0] - width / 2.0, + center_point[1] + height / 2.0, + ), # upper left + ( + center_point[0] + width / 2.0, + center_point[1] + height / 2.0, + ), # upper right + ( + center_point[0] + (casing_thickness + width / 2.0), + center_point[1] + (casing_thickness + height / 2.0), + ), + ( + center_point[0] + (casing_thickness + width / 2.0), + center_point[1] - (casing_thickness + height / 2.0), + ), + ( + center_point[0] - (casing_thickness + width / 2.0), + center_point[1] - (casing_thickness + height / 2.0), + ), + ( + center_point[0] - (casing_thickness + width / 2.0), + center_point[1] + (casing_thickness + height / 2.0), + ), + ( + center_point[0] + (casing_thickness + width / 2.0), + center_point[1] + (casing_thickness + height / 2.0), + ) + ] + + self.points = all_points + + def create_solid(self): + """Creates a 3d solid using points with straight edges. + + Returns: + A CadQuery solid: A 3D solid volume + """ + + iter_points = iter(self.points) + pf_coils_set = [] + for p1, p2, p3, p4, p5, p6, p7, p8, p9, p10 in zip( + iter_points, iter_points, iter_points, iter_points, + iter_points, iter_points, iter_points, iter_points, + iter_points, iter_points, + ): + + solid = ( + cq.Workplane(self.workplane) + .polyline( + [p1[:2], p2[:2], p3[:2], p4[:2], p5[:2], p6[:2], + p7[:2], p8[:2], p9[:2], p10[:2]]) + .close() + .revolve(self.rotation_angle) + ) + pf_coils_set.append(solid) + + compound = cq.Compound.makeCompound( + [a.val() for a in pf_coils_set] + ) + + self.solid = compound + + return compound diff --git a/paramak/parametric_components/poloidal_field_coil_case_set_fc.py b/paramak/parametric_components/poloidal_field_coil_case_set_fc.py new file mode 100644 index 000000000..3be5ba3c7 --- /dev/null +++ b/paramak/parametric_components/poloidal_field_coil_case_set_fc.py @@ -0,0 +1,191 @@ + +import cadquery as cq +from paramak import PoloidalFieldCoilSet, RotateStraightShape + + +class PoloidalFieldCoilCaseSetFC(RotateStraightShape): + """Creates a series of rectangular poloidal field coils. + + Args: + pf_coils (paramak.PoloidalFieldCoil): a list of pf coil objects or a + CadQuery compound object + casing_thicknesses (float or list of floats): the thicknesses of the + coil casing (cm). If float then the same thickness is applied to + all coils. If list of floats then each entry is applied to a + seperate pf_coil, one entry for each pf_coil. + stp_filename (str, optional): defaults to "PoloidalFieldCoilCaseSetFC.stp". + stl_filename (str, optional): defaults to "PoloidalFieldCoilCaseSetFC.stl". + name (str, optional): defaults to "pf_coil_case_set_fc". + material_tag (str, optional): defaults to "pf_coil_mat". + """ + + def __init__( + self, + pf_coils, + casing_thicknesses, + stp_filename="PoloidalFieldCoilCaseSetFC.stp", + stl_filename="PoloidalFieldCoilCaseSetFC.stl", + name="pf_coil_case_set_fc", + material_tag="pf_coil_mat", + **kwargs + ): + + super().__init__( + name=name, + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + self.casing_thicknesses = casing_thicknesses + self.pf_coils = pf_coils + + # calculated internally by the class + self.heights = None + self.widths = None + self.center_points = None + + @property + def casing_thicknesses(self): + return self._casing_thicknesses + + @casing_thicknesses.setter + def casing_thicknesses(self, value): + if isinstance(value, list): + if not all(isinstance(x, (int, float)) for x in value): + raise ValueError( + "Every entry in Casing_thicknesses must be a float or int") + else: + if not isinstance(value, (float, int)): + raise ValueError( + "Casing_thicknesses must be a list of numbers or a number") + self._casing_thicknesses = value + + @property + def pf_coils(self): + return self._pf_coils + + @pf_coils.setter + def pf_coils(self, value): + if not isinstance(value, (list, PoloidalFieldCoilSet)): + raise ValueError( + "PoloidalFieldCoilCaseSetFC.pf_coils must be either a list \ + paramak.PoloidalFieldCoil or a \ + paramak.PoloidalFieldCoilSet object") + self._pf_coils = value + + def find_points(self): + """Finds the XZ points joined by straight connections that describe + the 2D profile of the poloidal field coil shape.""" + + if isinstance(self.pf_coils, list): + self.heights = [entry.height for entry in self.pf_coils] + self.widths = [entry.width for entry in self.pf_coils] + self.center_points = [ + entry.center_point for entry in self.pf_coils] + + num_of_coils = len(self.pf_coils) + + elif isinstance(self.pf_coils, PoloidalFieldCoilSet): + self.heights = self.pf_coils.heights + self.widths = self.pf_coils.widths + self.center_points = self.pf_coils.center_points + + num_of_coils = len(self.pf_coils.solid.Solids()) + + if isinstance(self.casing_thicknesses, list): + if len(self.casing_thicknesses) != num_of_coils: + raise ValueError("The number pf_coils is not equal to the" + "number of thichnesses provided") + casing_thicknesses_list = self.casing_thicknesses + else: + casing_thicknesses_list = [self.casing_thicknesses] * num_of_coils + + all_points = [] + + for height, width, center_point, casing_thickness in zip( + self.heights, self.widths, + self.center_points, casing_thicknesses_list): + + if casing_thickness != 0: + + all_points = all_points + [ + ( + center_point[0] + width / 2.0, + center_point[1] + height / 2.0, + ), # upper right + ( + center_point[0] + width / 2.0, + center_point[1] - height / 2.0, + ), # lower right + ( + center_point[0] - width / 2.0, + center_point[1] - height / 2.0, + ), # lower left + ( + center_point[0] - width / 2.0, + center_point[1] + height / 2.0, + ), # upper left + ( + center_point[0] + width / 2.0, + center_point[1] + height / 2.0, + ), # upper right + ( + center_point[0] + (casing_thickness + width / 2.0), + center_point[1] + (casing_thickness + height / 2.0), + ), + ( + center_point[0] + (casing_thickness + width / 2.0), + center_point[1] - (casing_thickness + height / 2.0), + ), + ( + center_point[0] - (casing_thickness + width / 2.0), + center_point[1] - (casing_thickness + height / 2.0), + ), + ( + center_point[0] - (casing_thickness + width / 2.0), + center_point[1] + (casing_thickness + height / 2.0), + ), + ( + center_point[0] + (casing_thickness + width / 2.0), + center_point[1] + (casing_thickness + height / 2.0), + ) + ] + + self.points = all_points + + def create_solid(self): + """Creates a 3d solid using points with straight edges. Individual + solids in the compound can be accessed using .Solids()[i] where i is an + int + + Returns: + A CadQuery solid: A 3D solid volume + """ + + iter_points = iter(self.points) + pf_coils_set = [] + for p1, p2, p3, p4, p5, p6, p7, p8, p9, p10 in zip( + iter_points, iter_points, iter_points, iter_points, + iter_points, iter_points, iter_points, iter_points, + iter_points, iter_points, + ): + + solid = ( + cq.Workplane(self.workplane) + .polyline( + [p1[:2], p2[:2], p3[:2], p4[:2], p5[:2], p6[:2], + p7[:2], p8[:2], p9[:2], p10[:2]]) + .close() + .revolve(self.rotation_angle) + ) + pf_coils_set.append(solid) + + compound = cq.Compound.makeCompound( + [a.val() for a in pf_coils_set] + ) + + self.solid = compound + + return compound diff --git a/paramak/parametric_components/poloidal_field_coil_fp.py b/paramak/parametric_components/poloidal_field_coil_fp.py new file mode 100644 index 000000000..83b0024c9 --- /dev/null +++ b/paramak/parametric_components/poloidal_field_coil_fp.py @@ -0,0 +1,43 @@ + +from paramak import PoloidalFieldCoil + + +class PoloidalFieldCoilFP(PoloidalFieldCoil): + """Creates a rectangular poloidal field coil. + + Args: + corner_points (list of float tuples): the coordinates of the opposite + corners of the rectangular shaped coil e.g [(x1, y1), (x2, y2)] + """ + + def __init__( + self, + corner_points, + **kwargs + ): + + height = abs(corner_points[0][1] - corner_points[1][1]) + width = abs(corner_points[0][0] - corner_points[1][0]) + + center_width = (corner_points[0][1] + corner_points[1][1]) / 2. + center_height = (corner_points[0][1] + corner_points[1][1]) / 2. + center_point = (center_width, center_height) + + super().__init__( + height=height, + width=width, + center_point=center_point, + **kwargs + ) + + self.corner_points = corner_points + + @property + def corner_points(self): + return self._corner_points + + @corner_points.setter + def corner_points(self, value): + # ToDo check the corner points are a list with two entries + # and each entry is a tuple with two floats + self._corner_points = value diff --git a/paramak/parametric_components/poloidal_field_coil_set.py b/paramak/parametric_components/poloidal_field_coil_set.py new file mode 100644 index 000000000..33b743f11 --- /dev/null +++ b/paramak/parametric_components/poloidal_field_coil_set.py @@ -0,0 +1,144 @@ + +import cadquery as cq +from paramak import RotateStraightShape + + +class PoloidalFieldCoilSet(RotateStraightShape): + """Creates a series of rectangular poloidal field coils. + + Args: + heights (list of floats): the vertical (z axis) heights of the coils + (cm). + widths (list of floats): the horizontal (x axis) widths of the coils + (cm). + center_points (list of tuple of floats): the center of the coil (x,z) + values e.g. [(100,100), (100,200)] (cm). + stp_filename (str, optional): defaults to "PoloidalFieldCoilSet.stp". + stl_filename (str, optional): defaults to "PoloidalFieldCoilSet.stl". + name (str, optional): defaults to "pf_coil". + material_tag (str, optional): defaults to "pf_coil_mat". + """ + + def __init__( + self, + heights, + widths, + center_points, + stp_filename="PoloidalFieldCoilSet.stp", + stl_filename="PoloidalFieldCoilSet.stl", + name="pf_coil", + material_tag="pf_coil_mat", + **kwargs + ): + + super().__init__( + name=name, + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + self.center_points = center_points + self.heights = heights + self.widths = widths + + if len( + self.widths) != len( + self.heights) or len( + self.heights) != len( + self.center_points): + raise ValueError("The length of widthts, height and center_points \ + must be the same when making a PoloidalFieldCoilSet") + + @property + def center_points(self): + return self._center_points + + @center_points.setter + def center_points(self, value): + if not isinstance(value, list): + raise ValueError( + "PoloidalFieldCoilSet center_points must be a list") + self._center_points = value + + @property + def heights(self): + return self._heights + + @heights.setter + def heights(self, value): + if not isinstance(value, list): + raise ValueError("PoloidalFieldCoilSet heights must be a list") + self._heights = value + + @property + def widths(self): + return self._widths + + @widths.setter + def widths(self, value): + if not isinstance(value, list): + raise ValueError("PoloidalFieldCoilSet widths must be a list") + self._widths = value + + def find_points(self): + """Finds the XZ points joined by straight connections that describe + the 2D profile of the poloidal field coil shape.""" + + all_points = [] + + for height, width, center_point in zip( + self.heights, self.widths, self.center_points): + + all_points = all_points + [ + ( + center_point[0] + width / 2.0, + center_point[1] + height / 2.0, + ), # upper right + ( + center_point[0] + width / 2.0, + center_point[1] - height / 2.0, + ), # lower right + ( + center_point[0] - width / 2.0, + center_point[1] - height / 2.0, + ), # lower left + ( + center_point[0] - width / 2.0, + center_point[1] + height / 2.0, + ), # upper left + ] + + self.points = all_points + + def create_solid(self): + """Creates a 3d solid using points with straight connections + edges, azimuth_placement_angle and rotation angle. + individual solids in the compound can be accessed using .Solids()[i] + where i is an int + + Returns: + A CadQuery solid: A 3D solid volume + """ + + iter_points = iter(self.points) + pf_coils_set = [] + for p1, p2, p3, p4 in zip( + iter_points, iter_points, iter_points, iter_points): + + solid = ( + cq.Workplane(self.workplane) + .polyline([p1[:2], p2[:2], p3[:2], p4[:2]]) + .close() + .revolve(self.rotation_angle) + ) + pf_coils_set.append(solid) + + compound = cq.Compound.makeCompound( + [a.val() for a in pf_coils_set] + ) + + self.solid = compound + + return compound diff --git a/paramak/parametric_components/poloidal_segmenter.py b/paramak/parametric_components/poloidal_segmenter.py new file mode 100644 index 000000000..fa42d1c10 --- /dev/null +++ b/paramak/parametric_components/poloidal_segmenter.py @@ -0,0 +1,178 @@ + +import math + +import cadquery as cq +from paramak import RotateStraightShape +from paramak.utils import (coefficients_of_line_from_points, get_hash, + intersect_solid, rotate) + + +class PoloidalSegments(RotateStraightShape): + """Creates a ring of wedges from a central point. When provided with a + shape_to_segment the shape will be segmented by the wedges. This is useful + for segmenting geometry into equal poloidal angles. Intended to segment the + firstwall geometry for using in neutron wall loading simulations. + + Args: + center_point (tuple of floats): the center of the segmentation wedges + (x,z) values (cm). + shape_to_segment (paramak.Shape, optional): the Shape to segment, if + None then the segmenting solids will be returned. Defaults to None. + number_of_segments (int, optional): the number of equal angles + segments in 360 degrees. Defaults to 10. + max_distance_from_center (float): the maximum distance from the center + point outwards (cm). Defaults to 1000.0. + stp_filename (str, optional): defaults to "PoloidalSegmenter.stp". + stl_filename (str, optional): defaults to "PoloidalSegmenter.stl". + name (str, optional): defaults to "poloidal_segmenter". + material_tag (str, optional): defaults to "poloidal_segmenter_mat". + """ + + def __init__( + self, + center_point, + shape_to_segment=None, + number_of_segments=10, + max_distance_from_center=1000.0, + stp_filename="PoloidalSegmenter.stp", + stl_filename="PoloidalSegmenter.stl", + name="poloidal_segmenter", + material_tag="poloidal_segmenter_mat", + **kwargs + ): + + super().__init__( + name=name, + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + self.center_point = center_point + self.shape_to_segment = shape_to_segment + self.number_of_segments = number_of_segments + self.max_distance_from_center = max_distance_from_center + + @property + def number_of_segments(self): + return self._number_of_segments + + @number_of_segments.setter + def number_of_segments(self, value): + if isinstance(value, int) is False: + raise ValueError( + "PoloidalSegmenter.number_of_segments must be an int.") + if value < 1: + raise ValueError( + "PoloidalSegmenter.number_of_segments must be a minimum of 1.") + self._number_of_segments = value + + @property + def shape_to_segment(self): + return self._shape_to_segment + + @shape_to_segment.setter + def shape_to_segment(self, value): + self._shape_to_segment = value + + @property + def center_point(self): + return self._center_point + + @center_point.setter + def center_point(self, center_point): + self._center_point = center_point + + @property + def max_distance_from_center(self): + return self._max_distance_from_center + + @max_distance_from_center.setter + def max_distance_from_center(self, value): + self._max_distance_from_center = value + + def find_points(self): + """Finds the XZ points joined by straight connections that describe + the 2D profile of the poloidal segmentation shape.""" + + angle_per_segment = 360. / self.number_of_segments + + points = [] + + current_angle = 0 + + outer_point = ( + self.center_point[0] + + self.max_distance_from_center, + self.center_point[1]) + for i in range(self.number_of_segments): + + points.append(self.center_point) + + outer_point_1 = rotate( + self.center_point, + outer_point, + math.radians(current_angle) + ) + + outer_point_2 = rotate( + self.center_point, + outer_point, + math.radians(current_angle + angle_per_segment) + ) + + # if the point goes beyond the zero line then set to zero + for new_point in [outer_point_1, outer_point_2]: + if new_point[0] < 0: + m, c = coefficients_of_line_from_points( + new_point, self.center_point) + points.append((0, c)) + else: + points.append(new_point) + + current_angle = current_angle + angle_per_segment + + self.points = points + + def create_solid(self): + """Creates a 3d solid using points with straight edges. Individual + solids in the compound can be accessed using .Solids()[i] where i is an + int. + + Returns: + A CadQuery solid: A 3D solid volume + """ + + iter_points = iter(self.points) + triangle_wedges = [] + for p1, p2, p3 in zip(iter_points, iter_points, iter_points): + + solid = ( + cq.Workplane(self.workplane) + .polyline([p1[:2], p2[:2], p3[:2]]) + .close() + .revolve(self.rotation_angle) + ) + triangle_wedges.append(solid) + + if self.shape_to_segment is None: + + compound = cq.Compound.makeCompound( + [a.val() for a in triangle_wedges] + ) + + else: + + intersected_solids = [] + for segment in triangle_wedges: + overlap = intersect_solid(segment, self.shape_to_segment) + intersected_solids.append(overlap) + + compound = cq.Compound.makeCompound( + [a.val() for a in intersected_solids] + ) + + self.solid = compound + + return compound diff --git a/paramak/parametric_components/port_cutters_circular.py b/paramak/parametric_components/port_cutters_circular.py new file mode 100644 index 000000000..b6aa508d0 --- /dev/null +++ b/paramak/parametric_components/port_cutters_circular.py @@ -0,0 +1,54 @@ + +from paramak import ExtrudeCircleShape + + +class PortCutterCircular(ExtrudeCircleShape): + """Creates an extruded shape with a circular section that is used to cut + other components (eg. blanket, vessel,..) in order to create ports. + + Args: + z_pos (float): Z position (cm) of the port + height (float): height (cm) of the port + width (float): width (cm) of the port + distance (float): extruded distance (cm) of the cutter + stp_filename (str, optional): defaults to "PortCutterCircular.stp". + stl_filename (str, optional): defaults to "PortCutterCircular.stl". + name (str, optional): defaults to "circular_port_cutter". + material_tag (str, optional): defaults to "circular_port_cutter_mat". + extrusion_start_offset (float, optional): the distance between 0 and + the start of the extrusion. Defaults to 1.. + """ + + def __init__( + self, + z_pos, + radius, + distance, + workplane="ZY", + rotation_axis="Z", + extrusion_start_offset=1., + stp_filename="PortCutterCircular.stp", + stl_filename="PortCutterCircular.stl", + name="circular_port_cutter", + material_tag="circular_port_cutter_mat", + **kwargs + ): + super().__init__( + workplane=workplane, + rotation_axis=rotation_axis, + extrusion_start_offset=extrusion_start_offset, + radius=radius, + extrude_both=False, + name=name, + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + distance=distance, + **kwargs + ) + + self.z_pos = z_pos + self.radius = radius + + def find_points(self): + self.points = [(0, self.z_pos)] diff --git a/paramak/parametric_components/port_cutters_rectangular.py b/paramak/parametric_components/port_cutters_rectangular.py new file mode 100644 index 000000000..4ac5fc9b4 --- /dev/null +++ b/paramak/parametric_components/port_cutters_rectangular.py @@ -0,0 +1,73 @@ + +from paramak import ExtrudeStraightShape + + +class PortCutterRectangular(ExtrudeStraightShape): + """Creates an extruded shape with a rectangular section that is used to cut + other components (eg. blanket, vessel,..) in order to create ports. + + Args: + z_pos (float): Z position (cm) of the port + height (float): height (cm) of the port + width (float): width (cm) of the port + distance (float): extruded distance (cm) of the cutter + fillet_radius (float, optional): If not None, radius (cm) of fillets + added to edges orthogonal to the Z direction. Defaults to None. + stp_filename (str, optional): defaults to "PortCutterRectangular.stp". + stl_filename (str, optional): defaults to "PortCutterRectangular.stl". + name (str, optional): defaults to "rectangular_port_cutter". + material_tag (str, optional): defaults to + "rectangular_port_cutter_mat". + extrusion_start_offset (float, optional): the distance between 0 and + the start of the extrusion. Defaults to 1.. + """ + + def __init__( + self, + z_pos, + height, + width, + distance, + workplane="ZY", + rotation_axis="Z", + extrusion_start_offset=1., + fillet_radius=None, + stp_filename="PortCutterRectangular.stp", + stl_filename="PortCutterRectangular.stl", + name="rectangular_port_cutter", + material_tag="rectangular_port_cutter_mat", + **kwargs + ): + + super().__init__( + workplane=workplane, + rotation_axis=rotation_axis, + extrusion_start_offset=extrusion_start_offset, + extrude_both=False, + name=name, + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + distance=distance, + **kwargs + ) + + self.z_pos = z_pos + self.height = height + self.width = width + self.fillet_radius = fillet_radius + self.add_fillet() + + def find_points(self): + points = [ + (-self.width / 2, -self.height / 2), + (self.width / 2, -self.height / 2), + (self.width / 2, self.height / 2), + (-self.width / 2, self.height / 2), + ] + points = [(e[0], e[1] + self.z_pos) for e in points] + self.points = points + + def add_fillet(self): + if self.fillet_radius is not None and self.fillet_radius != 0: + self.solid = self.solid.edges('#Z').fillet(self.fillet_radius) diff --git a/paramak/parametric_components/port_cutters_rotated.py b/paramak/parametric_components/port_cutters_rotated.py new file mode 100644 index 000000000..4651b8a91 --- /dev/null +++ b/paramak/parametric_components/port_cutters_rotated.py @@ -0,0 +1,123 @@ + +import math + +from paramak import RotateStraightShape +from paramak.utils import coefficients_of_line_from_points, rotate + + +class PortCutterRotated(RotateStraightShape): + """Creates wedges from a central point with angular extent in polar + direction. To control the width the rotation_angle argument can be used. + This is useful as a cutting volume for the creation of ports in blankets. + + Args: + center_point (tuple of floats): the center point where the + ports are aimed towards, typically the center of the plasma. + polar_coverage_angle (float): the angular extent of port in the + polar direction (degrees). Defaults to 10.0. + polar_placement_angle (float): The angle used when rotating the shape + on the polar axis. 0 degrees is the outboard equatorial point. + Defaults to 0.0. + max_distance_from_center (float): the maximum distance from the center + point outwards (cm). Default 3000.0. + fillet_radius (float, optional): If not None, radius (cm) of fillets + added to all edges. Defaults to 0.0. + rotation_angle (float, optional): defaults to 10.0. + stp_filename (str, optional): defaults to "PortCutter.stp". + stl_filename (str, optional): defaults to "PortCutter.stl". + name (str, optional): defaults to "port_cutter". + material_tag (str, optional): defaults to "port_cutter_mat". + """ + + def __init__( + self, + center_point, + polar_coverage_angle=10.0, + polar_placement_angle=0.0, + max_distance_from_center=3000.0, + fillet_radius=0.0, + rotation_angle=10.0, + stp_filename="PortCutter.stp", + stl_filename="PortCutter.stl", + name="port_cutter", + material_tag="port_cutter_mat", + **kwargs + ): + + super().__init__( + name=name, + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + rotation_angle=rotation_angle, + **kwargs + ) + + self.center_point = center_point + self.polar_coverage_angle = polar_coverage_angle + self.polar_placement_angle = polar_placement_angle + self.max_distance_from_center = max_distance_from_center + self.fillet_radius = fillet_radius + + self.add_fillet() + + @property + def polar_coverage_angle(self): + return self._polar_coverage_angle + + @polar_coverage_angle.setter + def polar_coverage_angle(self, value): + if value > 180: + msg = "polar_coverage_angle must be greater than 180.0" + raise ValueError(msg) + self._polar_coverage_angle = value + + @property + def center_point(self): + return self._center_point + + @center_point.setter + def center_point(self, center_point): + self._center_point = center_point + + @property + def max_distance_from_center(self): + return self._max_distance_from_center + + @max_distance_from_center.setter + def max_distance_from_center(self, value): + self._max_distance_from_center = value + + def find_points(self): + """Finds the XZ points joined by straight connections that describe the + 2D profile of the port cutter.""" + + points = [self.center_point] + + outer_point = (self.center_point[0] + self.max_distance_from_center, + self.center_point[1]) + + outer_point_rotated = rotate( + self.center_point, + outer_point, + math.radians(self.polar_placement_angle)) + + outer_point_1 = rotate( + self.center_point, + outer_point_rotated, + math.radians(0.5 * self.polar_coverage_angle)) + + outer_point_2 = rotate( + self.center_point, + outer_point_rotated, + math.radians(-0.5 * self.polar_coverage_angle)) + + points.append(outer_point_1) + points.append(outer_point_2) + + self.points = points + + def add_fillet(self): + """adds fillets to all edges""" + if self.fillet_radius != 0: + self.solid = self.solid.edges().fillet(self.fillet_radius) diff --git a/paramak/parametric_components/tf_coil_casing.py b/paramak/parametric_components/tf_coil_casing.py new file mode 100644 index 000000000..87c84debd --- /dev/null +++ b/paramak/parametric_components/tf_coil_casing.py @@ -0,0 +1,104 @@ + +import warnings + +from paramak import ExtrudeMixedShape, ExtrudeStraightShape +from paramak.utils import add_thickness, cut_solid, union_solid + + +class TFCoilCasing(ExtrudeMixedShape): + def __init__(self, magnet, inner_offset, outer_offset, + vertical_section_offset, **kwargs): + self.magnet = magnet + super().__init__(**kwargs) + self.inner_offset = inner_offset + self.outer_offset = outer_offset + self.vertical_section_offset = vertical_section_offset + self.leg_shape = ExtrudeStraightShape( + distance=self.distance, + azimuth_placement_angle=self.azimuth_placement_angle) + + @property + def azimuth_placement_angle(self): + self.azimuth_placement_angle = self.magnet.azimuth_placement_angle + return self._azimuth_placement_angle + + @azimuth_placement_angle.setter + def azimuth_placement_angle(self, value): + correct_angles = self.magnet.azimuth_placement_angle + if value != correct_angles: + msg = "Casing azimuth_placement_angle should be the" + \ + " same value as TFCoilCasing.magnet." + warnings.warn(msg, UserWarning) + self._azimuth_placement_angle = correct_angles + + def find_points(self): + inner_points_magnet = self.magnet.inner_points + outer_points_magnet = self.magnet.outer_points + inner_points_magnet = ( + inner_points_magnet[:, 0], + inner_points_magnet[:, 1] + ) + outer_points_magnet = ( + outer_points_magnet[:, 0], + outer_points_magnet[:, 1] + ) + inner_points = add_thickness( + *inner_points_magnet, + thickness=-self.inner_offset, + ) + + outer_points = add_thickness( + *outer_points_magnet, + thickness=-self.outer_offset + ) + curve_points = [] + for distrib_points in [inner_points, outer_points]: + curve_points.append([[R, Z, 'spline'] for R, Z in zip( + distrib_points[0], distrib_points[1])]) + + curve_points[0][-1][2] = 'straight' + curve_points[1][-1][2] = "straight" + + points = curve_points[0] + curve_points[1] + self.points = points + + yA = outer_points[1][outer_points[0].index( + min(outer_points[0], key=lambda x:abs(x - min(inner_points[0]))))] + self.leg_points = [ + ( + min(outer_points[0]) - self.vertical_section_offset, + min(outer_points[1])), + ( + outer_points[0][outer_points[1].index(min(outer_points[1]))], + min(outer_points[1])), + ( + inner_points[0][inner_points[1].index(min(inner_points[1]))], + min(inner_points[1])), + (min(inner_points[0]), min( + yA, self.magnet.vertical_displacement - yA)), + # not having this line avoid unexpected surfaces + (inner_points[0][-1], inner_points[1][-1]), + (min(inner_points[0]), max( + yA, self.magnet.vertical_displacement - yA)), + ( + inner_points[0][inner_points[1].index(min(inner_points[1]))], + max(inner_points[1])), + ( + outer_points[0][outer_points[1].index(min(outer_points[1]))], + max(outer_points[1])), + ( + min(outer_points[0]) - self.vertical_section_offset, + max(outer_points[1])), + ] + + def create_solid(self): + solid = super().create_solid() + + self.leg_shape.points = self.leg_points + self.leg_shape.distance = self.distance + self.leg_shape.azimuth_placement_angle = \ + self.azimuth_placement_angle + solid = union_solid(solid, self.leg_shape) + solid = cut_solid(solid, self.magnet) + self.solid = solid + return solid diff --git a/paramak/parametric_components/tokamak_plasma.py b/paramak/parametric_components/tokamak_plasma.py new file mode 100644 index 000000000..36a4e189f --- /dev/null +++ b/paramak/parametric_components/tokamak_plasma.py @@ -0,0 +1,230 @@ + +import numpy as np +from paramak import RotateSplineShape + + +class Plasma(RotateSplineShape): + """Creates a double null tokamak plasma shape that is controlled by 4 + shaping parameters. + + Args: + elongation (float, optional): the elongation of the plasma. + Defaults to 2.0. + major_radius (float, optional): the major radius of the plasma (cm). + Defaults to 450.0. + minor_radius (int, optional): the minor radius of the plasma (cm). + Defaults to 150.0. + triangularity (float, optional): the triangularity of the plasma. + Defaults to 0.55. + vertical_displacement (float, optional): the vertical_displacement + of the plasma (cm). Defaults to 0.0. + num_points (int, optional): number of points to describe the + shape. Defaults to 50. + configuration (str, optional): plasma configuration + ("non-null", "single-null", "double-null"). + Defaults to "non-null". + x_point_shift (float, optional): shift parameters for locating the + X points in [0, 1]. Defaults to 0.1. + name (str, optional): Defaults to "plasma". + material_tag (str, optional): defaults to "DT_plasma". + stp_filename (str, optional): defaults to "plasma.stp". + stl_filename (str, optional): defaults to "plasma.stl". + """ + + def __init__( + self, + elongation=2.0, + major_radius=450.0, + minor_radius=150.0, + triangularity=0.55, + vertical_displacement=0.0, + num_points=50, + configuration="non-null", + x_point_shift=0.1, + name="plasma", + material_tag="DT_plasma", + stp_filename="plasma.stp", + stl_filename="plasma.stl", + **kwargs + ): + + super().__init__( + name=name, + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + # properties needed for plasma shapes + self.elongation = elongation + self.major_radius = major_radius + self.minor_radius = minor_radius + self.triangularity = triangularity + self.vertical_displacement = vertical_displacement + self.num_points = num_points + self.configuration = configuration + self.x_point_shift = x_point_shift + + self.outer_equatorial_point = None + self.inner_equatorial_point = None + self.high_point = None + self.low_point = None + + @property + def high_point(self): + self.high_point = ( + self.major_radius - self.triangularity * self.minor_radius, + self.elongation * self.minor_radius + self.vertical_displacement, + ) + return self._high_point + + @high_point.setter + def high_point(self, value): + self._high_point = value + + @property + def low_point(self): + self.low_point = ( + self.major_radius - self.triangularity * self.minor_radius, + -self.elongation * self.minor_radius + self.vertical_displacement, + ) + return self._low_point + + @low_point.setter + def low_point(self, value): + self._low_point = value + + @property + def outer_equatorial_point(self): + self.outer_equatorial_point = ( + self.major_radius + self.minor_radius, self.vertical_displacement) + return self._outer_equatorial_point + + @outer_equatorial_point.setter + def outer_equatorial_point(self, value): + self._outer_equatorial_point = value + + @property + def inner_equatorial_point(self): + self.inner_equatorial_point = ( + self.major_radius - self.minor_radius, self.vertical_displacement) + return self._inner_equatorial_point + + @inner_equatorial_point.setter + def inner_equatorial_point(self, value): + self._inner_equatorial_point = value + + @property + def lower_x_point(self): + self.compute_x_points() + return self._lower_x_point + + @lower_x_point.setter + def lower_x_point(self, value): + self._lower_x_point = value + + @property + def upper_x_point(self): + self.compute_x_points() + return self._upper_x_point + + @upper_x_point.setter + def upper_x_point(self, value): + self._upper_x_point = value + + @property + def vertical_displacement(self): + return self._vertical_displacement + + @vertical_displacement.setter + def vertical_displacement(self, value): + self._vertical_displacement = value + + @property + def minor_radius(self): + return self._minor_radius + + @minor_radius.setter + def minor_radius(self, value): + if value < 1: + raise ValueError("minor_radius is out of range") + else: + self._minor_radius = value + + @property + def major_radius(self): + return self._major_radius + + @major_radius.setter + def major_radius(self, value): + if value < 1: + raise ValueError("major_radius is out of range") + else: + self._major_radius = value + + @property + def elongation(self): + return self._elongation + + @elongation.setter + def elongation(self, value): + if value > 10 or value < 0: + raise ValueError("elongation is out of range") + else: + self._elongation = value + + def compute_x_points(self): + """Computes the location of X points based on plasma parameters and + configuration + + Returns: + ((float, float), (float, float)): lower and upper x points + coordinates. None if no x points + """ + lower_x_point, upper_x_point = None, None # non-null config + shift = self.x_point_shift + elongation = self.elongation + triangularity = self.triangularity + if self.configuration in ["single-null", "double-null"]: + # no X points for non-null config + lower_x_point = (1 - + (1 + + shift) * + triangularity * + self.minor_radius, - + (1 + + shift) * + elongation * + self.minor_radius + + self.vertical_displacement, ) + + if self.configuration == "double-null": + # upper_x_point is up-down symmetrical + upper_x_point = ( + lower_x_point[0], + (1 + shift) * elongation * self.minor_radius + + self.vertical_displacement, + ) + self.lower_x_point = lower_x_point + self.upper_x_point = upper_x_point + + def find_points(self): + """Finds the XZ points that describe the 2D profile of the plasma.""" + + # create array of angles theta + theta = np.linspace(0, 2 * np.pi, num=self.num_points, endpoint=False) + + # parametric equations for plasma + def R(theta): + return self.major_radius + self.minor_radius * np.cos( + theta + self.triangularity * np.sin(theta) + ) + + def Z(theta): + return ( + self.elongation * self.minor_radius * np.sin(theta) + + self.vertical_displacement + ) + + self.points = np.stack((R(theta), Z(theta)), axis=1).tolist() diff --git a/paramak/parametric_components/tokamak_plasma_from_points.py b/paramak/parametric_components/tokamak_plasma_from_points.py new file mode 100644 index 000000000..749b0ab52 --- /dev/null +++ b/paramak/parametric_components/tokamak_plasma_from_points.py @@ -0,0 +1,66 @@ + +from paramak import Plasma + + +class PlasmaFromPoints(Plasma): + """Creates a double null tokamak plasma shape that is controlled by 3 + coordinates. + + Args: + outer_equatorial_x_point (float): the x value of the outer equatorial + of the plasma (cm). + inner_equatorial_x_point (float): the x value of the inner equatorial + of the plasma (cm). + high_point (tuple of 2 floats): the (x,z) coordinate values of the top + of the plasma (cm). + """ + + def __init__( + self, + outer_equatorial_x_point, + inner_equatorial_x_point, + high_point, + **kwargs + ): + + minor_radius = (outer_equatorial_x_point - + inner_equatorial_x_point) / 2.0 + major_radius = inner_equatorial_x_point + minor_radius + elongation = high_point[1] / minor_radius + triangularity = (major_radius - high_point[0]) / minor_radius + + super().__init__( + elongation=elongation, + major_radius=major_radius, + minor_radius=minor_radius, + triangularity=triangularity, + **kwargs + ) + + self.outer_equatorial_x_point = outer_equatorial_x_point + self.inner_equatorial_x_point = inner_equatorial_x_point + self.high_point = high_point + + @property + def outer_equatorial_x_point(self): + return self._outer_equatorial_x_point + + @outer_equatorial_x_point.setter + def outer_equatorial_x_point(self, value): + self._outer_equatorial_x_point = value + + @property + def inner_equatorial_x_point(self): + return self._inner_equatorial_x_point + + @inner_equatorial_x_point.setter + def inner_equatorial_x_point(self, value): + self._inner_equatorial_x_point = value + + @property + def high_point(self): + return self._high_point + + @high_point.setter + def high_point(self, value): + self._high_point = value diff --git a/paramak/parametric_components/tokamak_plasma_plasmaboundaries.py b/paramak/parametric_components/tokamak_plasma_plasmaboundaries.py new file mode 100644 index 000000000..11fb88358 --- /dev/null +++ b/paramak/parametric_components/tokamak_plasma_plasmaboundaries.py @@ -0,0 +1,87 @@ + +from paramak import Plasma +from plasmaboundaries import get_separatrix_coordinates + + +class PlasmaBoundaries(Plasma): + """Creates a double null tokamak plasma shape that is controlled + by 5 shaping parameters using the plasmaboundaries package to calculate + points. For more details see: + http://github.com/RemDelaporteMathurin/plasma-boundaries + + Args: + A (float, optional): plasma parameter see plasmaboundaries doc. + Defaults to 0.05. + elongation (float, optional): the elongation of the plasma. + Defaults to 2.0. + major_radius (float, optional): the major radius of the plasma + (cm). Defaults to 450.0. + minor_radius (float, optional): the minor radius of the plasma + (cm). Defaults to 150.0. + triangularity (float, optional): the triangularity of the plasma. + Defaults to 0.55. + vertical_displacement (float, optional): the vertical_displacement + of the plasma (cm). Defaults to 0.0. + configuration (str, optional): plasma configuration + ("non-null", "single-null", "double-null"). + Defaults to "non-null". + x_point_shift (float, optional): Shift parameters for locating the + X points in [0, 1]. Defaults to 0.1. + """ + + def __init__( + self, + A=0.05, + elongation=2.0, + major_radius=450.0, + minor_radius=150.0, + triangularity=0.55, + vertical_displacement=0.0, + configuration="non-null", + x_point_shift=0.1, + **kwargs + ): + + super().__init__( + elongation=elongation, + major_radius=major_radius, + minor_radius=minor_radius, + triangularity=triangularity, + vertical_displacement=vertical_displacement, + configuration=configuration, + x_point_shift=x_point_shift, + **kwargs + ) + + # properties needed for plasma shapes + self.A = A + + def find_points(self): + """Finds the XZ points that describe the 2D profile of the plasma.""" + aspect_ratio = self.minor_radius / self.major_radius + params = { + "A": self.A, + "aspect_ratio": aspect_ratio, + "elongation": self.elongation, + "triangularity": self.triangularity, + } + points = get_separatrix_coordinates( + params, self.configuration) + # add vertical displacement + points[:, 1] += self.vertical_displacement + # rescale to cm + points[:] *= self.major_radius + + # remove unnecessary points + # if non-null these are the y bounds + lower_point_y = self.low_point[1] + upper_point_y = self.high_point[1] + # else use x points + if self.configuration in ["single-null", "double-null"]: + lower_point_y = self.lower_x_point[1] + if self.configuration == "double-null": + upper_point_y = self.upper_x_point[1] + + points = points[ + (points[:, 1] >= lower_point_y) & (points[:, 1] <= upper_point_y)] + self.points = points.tolist()[:-1] diff --git a/paramak/parametric_components/toroidal_field_coil_coat_hanger.py b/paramak/parametric_components/toroidal_field_coil_coat_hanger.py new file mode 100644 index 000000000..ecdafb05a --- /dev/null +++ b/paramak/parametric_components/toroidal_field_coil_coat_hanger.py @@ -0,0 +1,258 @@ + +import math + +import cadquery as cq +import numpy as np +from paramak import ExtrudeStraightShape +from paramak.utils import calculate_wedge_cut, rotate + + +class ToroidalFieldCoilCoatHanger(ExtrudeStraightShape): + """Creates a coat hanger shaped toroidal field coil. + + Args: + horizontal_start_point (tuple of 2 floats): the (x,z) coordinates of + the inner upper point (cm). + horizontal_length (tuple of 2 floats): the radial length of the + horizontal section of the TF coil (cm). + vertical_mid_point (tuple of 2 points): the (x,z) coordinates of the + mid point of the outboard vertical section (cm). + vertical_length (tuple of 2 floats): the radial length of the outboard + vertical section of the TF coil (cm). + thickness (float): the thickness of the toroidal field coil. + distance (float): the extrusion distance. + number_of_coils (int): the number of TF coils. This changes with + azimuth_placement_angle dividing up 360 degrees by the number of + coils. + with_inner_leg (bool, optional): Include the inner TF leg. Defaults to + True. + stp_filename (str, optional): defaults to + "ToroidalFieldCoilCoatHanger.stp". + stl_filename (str, optional): defaults to + "ToroidalFieldCoilCoatHanger.stl". + material_tag (str, optional): defaults to "outer_tf_coil_mat". + """ + + def __init__( + self, + horizontal_start_point, + horizontal_length, + vertical_mid_point, + vertical_length, + thickness, + distance, + number_of_coils, + with_inner_leg=True, + stp_filename="ToroidalFieldCoilCoatHanger.stp", + stl_filename="ToroidalFieldCoilCoatHanger.stl", + material_tag="outer_tf_coil_mat", + **kwargs + ): + + super().__init__( + distance=distance, + stp_filename=stp_filename, + stl_filename=stl_filename, + material_tag=material_tag, + **kwargs + ) + + self.horizontal_start_point = horizontal_start_point + self.horizontal_length = horizontal_length + self.vertical_mid_point = vertical_mid_point + self.vertical_length = vertical_length + self.thickness = thickness + self.distance = distance + self.number_of_coils = number_of_coils + self.with_inner_leg = with_inner_leg + + @property + def azimuth_placement_angle(self): + self.find_azimuth_placement_angle() + return self._azimuth_placement_angle + + @azimuth_placement_angle.setter + def azimuth_placement_angle(self, value): + self._azimuth_placement_angle = value + + def find_points(self): + """Finds the XZ points joined by straight connections that describe the 2D + profile of the poloidal field coil shape.""" + + # 16---15 + # - - + # - 14 + # - - + # 1---2 - + # - - + # - 13 + # - - + # 3 12 + # - - + # - - + # - - + # 4 11 + # - - + # - 10 + # - - + # 6---5 - + # - - + # - 9 + # - - + # 7---8 + + adjacent_length = self.vertical_mid_point[0] - ( + self.horizontal_start_point[0] + self.horizontal_length) + oppersite_length = self.horizontal_start_point[1] - ( + self.vertical_mid_point[1] + 0.5 * self.vertical_length) + + point_rotation = math.atan(oppersite_length / adjacent_length) + point_rotation_mid = math.radians(90) - point_rotation + + points = [ + self.horizontal_start_point, # point 1 + ( + self.horizontal_start_point[0] + self.horizontal_length, + self.horizontal_start_point[1], + ), # point 2 + ( + self.vertical_mid_point[0], + self.vertical_mid_point[1] + 0.5 * self.vertical_length, + ), # point 3 + ( + self.vertical_mid_point[0], + self.vertical_mid_point[1] - 0.5 * self.vertical_length, + ), # point 4 + ( + self.horizontal_start_point[0] + self.horizontal_length, + -self.horizontal_start_point[1], + ), # point 5 + ( + self.horizontal_start_point[0], + -self.horizontal_start_point[1], + ), # point 6 + ( + self.horizontal_start_point[0], + -self.horizontal_start_point[1] - self.thickness, + ), # point 7 + ( + self.horizontal_start_point[0] + self.horizontal_length, + -self.horizontal_start_point[1] - self.thickness, + ), # point 8 + rotate( + ( + self.horizontal_start_point[0] + self.horizontal_length, + -self.horizontal_start_point[1], + ), # same as point 5 + ( + self.horizontal_start_point[0] + self.horizontal_length, + -self.horizontal_start_point[1] - self.thickness, + ), # same as point 8 + point_rotation + ), # point 9 + rotate( + ( + self.vertical_mid_point[0], + self.vertical_mid_point[1] - 0.5 * self.vertical_length, + ), # same as point 4 + ( + self.vertical_mid_point[0] + self.thickness, + self.vertical_mid_point[1] - 0.5 * self.vertical_length, + ), # same as point 11 + -point_rotation_mid + ), # point 10 + ( + self.vertical_mid_point[0] + self.thickness, + self.vertical_mid_point[1] - 0.5 * self.vertical_length, + ), # point 11 + ( + self.vertical_mid_point[0] + self.thickness, + self.vertical_mid_point[1] + 0.5 * self.vertical_length, + ), # point 12 + rotate( + ( + self.vertical_mid_point[0], + self.vertical_mid_point[1] + 0.5 * self.vertical_length, + ), # same as point 3 + ( + self.vertical_mid_point[0] + self.thickness, + self.vertical_mid_point[1] + 0.5 * self.vertical_length, + ), # same as point 12 + point_rotation_mid + ), # point 13 + rotate( + ( + self.horizontal_start_point[0] + self.horizontal_length, + self.horizontal_start_point[1], + ), # same as point 2 + ( + self.horizontal_start_point[0] + self.horizontal_length, + self.horizontal_start_point[1] + self.thickness, + ), # same as point 15 + -point_rotation + ), # point 14 + ( + self.horizontal_start_point[0] + self.horizontal_length, + self.horizontal_start_point[1] + self.thickness, + ), # point 15 + ( + self.horizontal_start_point[0], + self.horizontal_start_point[1] + self.thickness, + ) # point 16 + ] + + self.inner_leg_connection_points = [ + points[0], + (points[0][0] + self.thickness, points[0][1]), + (points[5][0] + self.thickness, points[5][1]), + points[5], + ] + + self.points = points + + def find_azimuth_placement_angle(self): + """Calculates the azimuth placement angles based on the number of + toroidal field coils""" + + angles = list( + np.linspace( + 0, + 360, + self.number_of_coils, + endpoint=False)) + + self.azimuth_placement_angle = angles + + def create_solid(self): + """Creates a 3d solid using points with straight edges. + + Returns: + A CadQuery solid: A 3D solid volume + """ + + # Creates a cadquery solid from points and revolves + points_without_connections = [p[:2] for p in self.points] + solid = ( + cq.Workplane(self.workplane) + .polyline(points_without_connections) + .close() + .extrude(distance=-self.distance / 2.0, both=True) + ) + + if self.with_inner_leg is True: + inner_leg_solid = cq.Workplane(self.workplane) + inner_leg_solid = inner_leg_solid.polyline( + self.inner_leg_connection_points) + inner_leg_solid = inner_leg_solid.close().extrude( + distance=-self.distance / 2.0, both=True) + + solid = cq.Compound.makeCompound( + [a.val() for a in [inner_leg_solid, solid]] + ) + + solid = self.rotate_solid(solid) + cutting_wedge = calculate_wedge_cut(self) + solid = self.perform_boolean_operations(solid, wedge_cut=cutting_wedge) + self.solid = solid # not necessarily required as set in boolean_operations + + return solid diff --git a/paramak/parametric_components/toroidal_field_coil_princeton_d.py b/paramak/parametric_components/toroidal_field_coil_princeton_d.py new file mode 100644 index 000000000..06f04ece3 --- /dev/null +++ b/paramak/parametric_components/toroidal_field_coil_princeton_d.py @@ -0,0 +1,192 @@ + +import numpy as np +from paramak import ExtrudeMixedShape +from paramak.utils import add_thickness +from scipy import integrate +from scipy.optimize import minimize + + +class ToroidalFieldCoilPrincetonD(ExtrudeMixedShape): + """Toroidal field coil based on Princeton-D curve + + Args: + R1 (float): smallest radius (cm) + R2 (float): largest radius (cm) + thickness (float): magnet thickness (cm) + distance (float): extrusion distance (cm) + number_of_coils (int): the number of tf coils. This changes by the + azimuth_placement_angle dividing up 360 degrees by the number of + coils. + vertical_displacement (float, optional): vertical displacement (cm). + Defaults to 0.0. + with_inner_leg (bool, optional): Include the inner tf leg. Defaults to + True. + stp_filename (str, optional): defaults to + "ToroidalFieldCoilPrincetonD.stp". + stl_filename (str, optional): defaults to + "ToroidalFieldCoilPrincetonD.stl". + material_tag (str, optional): defaults to "outer_tf_coil_mat". + """ + + def __init__( + self, + R1, + R2, + thickness, + distance, + number_of_coils, + vertical_displacement=0.0, + with_inner_leg=True, + stp_filename="ToroidalFieldCoilPrincetonD.stp", + stl_filename="ToroidalFieldCoilPrincetonD.stl", + material_tag="outer_tf_coil_mat", + **kwargs + ): + + super().__init__( + distance=distance, + stp_filename=stp_filename, + stl_filename=stl_filename, + material_tag=material_tag, + **kwargs + ) + + self.R1 = R1 + self.R2 = R2 + self.thickness = thickness + self.distance = distance + self.number_of_coils = number_of_coils + self.vertical_displacement = vertical_displacement + self.with_inner_leg = with_inner_leg + + @property + def inner_points(self): + self.points + return self._inner_points + + @inner_points.setter + def inner_points(self, value): + self._inner_points = value + + @property + def outer_points(self): + self.points + return self._outer_points + + @outer_points.setter + def outer_points(self, value): + self._outer_points = value + + @property + def azimuth_placement_angle(self): + self.find_azimuth_placement_angle() + return self._azimuth_placement_angle + + @azimuth_placement_angle.setter + def azimuth_placement_angle(self, value): + self._azimuth_placement_angle = value + + def _compute_inner_points(self, R1, R2): + """Computes the inner curve points + + Args: + R1 (float): smallest radius (cm) + R2 (float): largest radius (cm) + + Returns: + (list, list, list): R, Z and derivative lists for outer curve + points + """ + def error(z_0, R0, R2): + segment = get_segment(R0, R2, z_0) + return abs(segment[1][-1]) + + def get_segment(a, b, z_0): + a_R = np.linspace(a, b, num=70, endpoint=True) + asol = integrate.odeint(solvr, [z_0, 0], a_R) + return a_R, asol[:, 0], asol[:, 1] + + def solvr(Y, R): + return [Y[1], -1 / (k * R) * (1 + Y[1]**2)**(3 / 2)] + + R0 = (R1 * R2)**0.5 + k = 0.5 * np.log(R2 / R1) + + # computing of z_0 + # z_0 is computed by ensuring outer segment end is zero + z_0 = 10 # initial guess for z_0 + res = minimize(error, z_0, args=(R0, R2)) + z_0 = res.x + + # compute inner and outer segments + segment1 = get_segment(R0, R1, z_0) + segment2 = get_segment(R0, R2, z_0) + + R = np.concatenate([np.flip(segment1[0]), segment2[0] + [1:], np.flip(segment2[0])[1:], segment1[0][1:]]) + Z = np.concatenate([np.flip(segment1[1]), segment2[1] + [1:], -np.flip(segment2[1])[1:], -segment1[1][1:]]) + return R, Z + + def find_points(self): + """Finds the XZ points joined by connections that describe the 2D + profile of the toroidal field coil shape.""" + # compute inner points + R_inner, Z_inner = self._compute_inner_points(self.R1, self.R2) + + # compute outer points + dz_dr = np.diff(Z_inner) / np.diff(R_inner) + dz_dr[0] = float("-inf") + dz_dr = np.append(dz_dr, float("inf")) + R_outer, Z_outer = add_thickness( + R_inner, Z_inner, self.thickness, dy_dx=dz_dr + ) + R_outer, Z_outer = np.flip(R_outer), np.flip(Z_outer) + + # add vertical displacement + Z_outer += self.vertical_displacement + Z_inner += self.vertical_displacement + + # extract helping points for inner leg + inner_leg_connection_points = [ + (R_inner[0], Z_inner[0]), + (R_inner[-1], Z_inner[-1]), + (R_outer[0], Z_outer[0]), + (R_outer[-1], Z_outer[-1]) + ] + self.inner_leg_connection_points = inner_leg_connection_points + + # add the leg to the points + if self.with_inner_leg: + R_inner = np.append(R_inner, R_inner[0]) + Z_inner = np.append(Z_inner, Z_inner[0]) + + R_outer = np.append(R_outer, R_outer[0]) + Z_outer = np.append(Z_outer, Z_outer[0]) + # add connections + inner_points = [[r, z, 'spline'] for r, z in zip(R_inner, Z_inner)] + outer_points = [[r, z, 'spline'] for r, z in zip(R_outer, Z_outer)] + if self.with_inner_leg: + outer_points[-2][2] = 'straight' + inner_points[-2][2] = 'straight' + + inner_points[-1][2] = 'straight' + outer_points[-1][2] = 'straight' + + points = inner_points + outer_points + self.outer_points = np.vstack((R_outer, Z_outer)).T + self.inner_points = np.vstack((R_inner, Z_inner)).T + self.points = points + + def find_azimuth_placement_angle(self): + """Calculates the azimuth placement angles based on the number of tf + coils""" + + angles = list( + np.linspace( + 0, + 360, + self.number_of_coils, + endpoint=False)) + + self.azimuth_placement_angle = angles diff --git a/paramak/parametric_components/toroidal_field_coil_rectangle.py b/paramak/parametric_components/toroidal_field_coil_rectangle.py new file mode 100644 index 000000000..3b0a7400a --- /dev/null +++ b/paramak/parametric_components/toroidal_field_coil_rectangle.py @@ -0,0 +1,157 @@ + +import cadquery as cq +import numpy as np +from paramak import ExtrudeStraightShape +from paramak.utils import calculate_wedge_cut + + +class ToroidalFieldCoilRectangle(ExtrudeStraightShape): + """Creates a rectangular shaped toroidal field coil. + + Args: + horizontal_start_point (tuple of 2 floats): the (x,z) coordinates of + the inner upper point (cm). + vertical_mid_point (tuple of 2 points): the (x,z) coordinates of the + mid point of the vertical section (cm). + thickness (float): the thickness of the toroidal field coil. + distance (float): the extrusion distance. + number_of_coils (int): the number of tf coils. This changes by the + azimuth_placement_angle dividing up 360 degrees by the number of + coils. + with_inner_leg (bool, optional): include the inner tf leg. Defaults to + True. + stp_filename (str, optional): defaults to + "ToroidalFieldCoilRectangle.stp". + stl_filename (str, optional): defaults to + "ToroidalFieldCoilRectangle.stl". + material_tag (str, optional): defaults to "outer_tf_coil_mat". + """ + + def __init__( + self, + horizontal_start_point, + vertical_mid_point, + thickness, + distance, + number_of_coils, + with_inner_leg=True, + stp_filename="ToroidalFieldCoilRectangle.stp", + stl_filename="ToroidalFieldCoilRectangle.stl", + material_tag="outer_tf_coil_mat", + **kwargs + ): + + super().__init__( + distance=distance, + stp_filename=stp_filename, + stl_filename=stl_filename, + material_tag=material_tag, + **kwargs + ) + + self.horizontal_start_point = horizontal_start_point + self.vertical_mid_point = vertical_mid_point + self.thickness = thickness + self.distance = distance + self.number_of_coils = number_of_coils + self.with_inner_leg = with_inner_leg + + @property + def azimuth_placement_angle(self): + self.find_azimuth_placement_angle() + return self._azimuth_placement_angle + + @azimuth_placement_angle.setter + def azimuth_placement_angle(self, value): + self._azimuth_placement_angle = value + + def find_points(self): + """Finds the XZ points joined by straight connections that describe + the 2D profile of the poloidal field coil shape.""" + + if self.horizontal_start_point[0] >= self.vertical_mid_point[0]: + raise ValueError( + 'horizontal_start_point x should be smaller than the \ + vertical_mid_point x value') + if self.vertical_mid_point[1] >= self.horizontal_start_point[1]: + raise ValueError( + 'vertical_mid_point y value should be smaller than the \ + horizontal_start_point y value') + + points = [ + self.horizontal_start_point, # connection point + # connection point + (self.horizontal_start_point[0] + + self.thickness, self.horizontal_start_point[1]), + (self.vertical_mid_point[0], self.horizontal_start_point[1]), + (self.vertical_mid_point[0], -self.horizontal_start_point[1]), + # connection point + (self.horizontal_start_point[0] + + self.thickness, - + self.horizontal_start_point[1]), + # connection point + (self.horizontal_start_point[0], -self.horizontal_start_point[1]), + (self.horizontal_start_point[0], - + (self.horizontal_start_point[1] + + self.thickness)), + (self.vertical_mid_point[0] + + self.thickness, - + (self.horizontal_start_point[1] + + self.thickness)), + (self.vertical_mid_point[0] + self.thickness, + self.horizontal_start_point[1] + self.thickness), + (self.horizontal_start_point[0], + self.horizontal_start_point[1] + self.thickness), + ] + + self.inner_leg_connection_points = [ + points[0], points[1], points[4], points[5]] + + self.points = points + + def find_azimuth_placement_angle(self): + """Calculates the azimuth placement angles based on the number of tf + coils""" + + angles = list( + np.linspace( + 0, + 360, + self.number_of_coils, + endpoint=False)) + + self.azimuth_placement_angle = angles + + def create_solid(self): + """Creates a 3d solid using points with straight edges. + + Returns: + A CadQuery solid: A 3D solid volume + """ + + # Creates a cadquery solid from points and revolves + points_without_connections = [p[:2] for p in self.points] + solid = ( + cq.Workplane(self.workplane) + .polyline(points_without_connections) + .close() + .extrude(distance=-self.distance / 2.0, both=True) + ) + + if self.with_inner_leg is True: + inner_leg_solid = cq.Workplane(self.workplane) + inner_leg_solid = inner_leg_solid.polyline( + self.inner_leg_connection_points) + inner_leg_solid = inner_leg_solid.close().extrude( + distance=-self.distance / 2.0, both=True) + + solid = cq.Compound.makeCompound( + [a.val() for a in [inner_leg_solid, solid]] + ) + + solid = self.rotate_solid(solid) + cutting_wedge = calculate_wedge_cut(self) + solid = self.perform_boolean_operations(solid, wedge_cut=cutting_wedge) + self.solid = solid # not necessarily required as set in boolean_operations + + return solid diff --git a/paramak/parametric_components/toroidal_field_coil_triple_arc.py b/paramak/parametric_components/toroidal_field_coil_triple_arc.py new file mode 100644 index 000000000..138b9fc6a --- /dev/null +++ b/paramak/parametric_components/toroidal_field_coil_triple_arc.py @@ -0,0 +1,177 @@ + +import numpy as np +from paramak import ExtrudeMixedShape + + +class ToroidalFieldCoilTripleArc(ExtrudeMixedShape): + """Toroidal field coil made of three arcs + + Args: + R1 (float): smallest radius (cm) + h (float): height of the straight section (cm) + radii ((float, float)): radii of the small and medium arcs (cm) + coverages ((float, float)): coverages of the small and medium arcs + (deg) + thickness (float): magnet thickness (cm) + distance (float): extrusion distance (cm) + number_of_coils (int): the number of TF coils. This changes by the + azimuth_placement_angle dividing up 360 degrees by the number of + coils. + vertical_displacement (float, optional): vertical displacement (cm). + Defaults to 0.0. + with_inner_leg (bool, optional): Include the inner tf leg. Defaults to + True. + stp_filename (str, optional): defaults to + "ToroidalFieldCoilTripleArc.stp". + stl_filename (str, optional): defaults to + "ToroidalFieldCoilTripleArc.stl". + material_tag (str, optional): defaults to "outer_tf_coil_mat". + """ + + def __init__( + self, + R1, + h, + radii, + coverages, + thickness, + distance, + number_of_coils, + vertical_displacement=0.0, + with_inner_leg=True, + stp_filename="ToroidalFieldCoilTripleArc.stp", + stl_filename="ToroidalFieldCoilTripleArc.stl", + material_tag="outer_tf_coil_mat", + **kwargs + ): + + super().__init__( + distance=distance, + stp_filename=stp_filename, + stl_filename=stl_filename, + material_tag=material_tag, + **kwargs + ) + self.R1 = R1 + self.h = h + self.small_radius, self.mid_radius = radii + + self.small_coverage, self.mid_coverage = coverages + self.thickness = thickness + self.distance = distance + self.number_of_coils = number_of_coils + self.vertical_displacement = vertical_displacement + self.with_inner_leg = with_inner_leg + + @property + def azimuth_placement_angle(self): + self.find_azimuth_placement_angle() + return self._azimuth_placement_angle + + @azimuth_placement_angle.setter + def azimuth_placement_angle(self, value): + self._azimuth_placement_angle = value + + def _compute_curve(self, R1, h, radii, coverages): + npoints = 500 + + small_radius, mid_radius = radii + small_coverage, mid_coverage = coverages + asum = small_coverage + mid_coverage + + # small arc + theta = np.linspace( + 0, small_coverage, round(0.5 * npoints * small_coverage / np.pi)) + small_arc_R = R1 + small_radius * (1 - np.cos(theta)) + small_arc_Z = h + small_radius * np.sin(theta) + + # mid arc + theta = np.linspace( + theta[-1], asum, round(0.5 * npoints * mid_coverage / np.pi)) + mid_arc_R = small_arc_R[-1] + mid_radius * \ + (np.cos(small_coverage) - np.cos(theta)) + mid_arc_Z = small_arc_Z[-1] + mid_radius * \ + (np.sin(theta) - np.sin(small_coverage)) + + # large arc + large_radius = (mid_arc_Z[-1]) / np.sin(np.pi - asum) + theta = np.linspace(theta[-1], np.pi, 60) + large_arc_R = mid_arc_R[-1] + large_radius * \ + (np.cos(np.pi - theta) - np.cos(np.pi - asum)) + large_arc_Z = mid_arc_Z[-1] - large_radius * \ + (np.sin(asum) - np.sin(np.pi - theta)) + + R = np.concatenate((small_arc_R, mid_arc_R[1:], large_arc_R[1:])) + R = np.append(R, np.flip(R)[1:]) + Z = np.concatenate((small_arc_Z, mid_arc_Z[1:], large_arc_Z[1:])) + Z = np.append(Z, -np.flip(Z)[1:]) + return R, Z + + def find_points(self): + """Finds the XZ points joined by connections that describe the 2D + profile of the toroidal field coil shape.""" + + thickness = self.thickness + small_radius, mid_radius = self.small_radius, self.mid_radius + small_coverage, mid_coverage = self.small_coverage, self.mid_coverage + small_coverage *= np.pi / 180 # convert to radians + mid_coverage *= np.pi / 180 + + # create inner coordinates + R_inner, Z_inner = self._compute_curve( + self.R1, self.h * 0.5, radii=(small_radius, mid_radius), + coverages=(small_coverage, mid_coverage)) + + # create outer coordinates + R_outer, Z_outer = self._compute_curve( + self.R1 - thickness, self.h * 0.5, + radii=(small_radius + thickness, mid_radius + thickness), + coverages=(small_coverage, mid_coverage)) + R_outer, Z_outer = np.flip(R_outer), np.flip(Z_outer) + + # add vertical displacement + Z_outer += self.vertical_displacement + Z_inner += self.vertical_displacement + + # extract helping points for inner leg + inner_leg_connection_points = [ + (R_inner[0], Z_inner[0]), + (R_inner[-1], Z_inner[-1]), + (R_outer[0], Z_outer[0]), + (R_outer[-1], Z_outer[-1]) + ] + self.inner_leg_connection_points = inner_leg_connection_points + + # add the leg to the points + if self.with_inner_leg: + R_inner = np.append(R_inner, R_inner[0]) + Z_inner = np.append(Z_inner, Z_inner[0]) + + R_outer = np.append(R_outer, R_outer[0]) + Z_outer = np.append(Z_outer, Z_outer[0]) + # add connections + inner_points = [[r, z, 'spline'] for r, z in zip(R_inner, Z_inner)] + outer_points = [[r, z, 'spline'] for r, z in zip(R_outer, Z_outer)] + if self.with_inner_leg: + outer_points[-2][2] = 'straight' + inner_points[-2][2] = 'straight' + + inner_points[-1][2] = 'straight' + outer_points[-1][2] = 'straight' + + points = inner_points + outer_points + + self.points = points + + def find_azimuth_placement_angle(self): + """Calculates the azimuth placement angles based on the number of tf + coils""" + + angles = list( + np.linspace( + 0, + 360, + self.number_of_coils, + endpoint=False)) + + self.azimuth_placement_angle = angles diff --git a/paramak/parametric_components/vacuum_vessel.py b/paramak/parametric_components/vacuum_vessel.py new file mode 100644 index 000000000..887a8da40 --- /dev/null +++ b/paramak/parametric_components/vacuum_vessel.py @@ -0,0 +1,75 @@ + +from paramak import RotateStraightShape + + +class VacuumVessel(RotateStraightShape): + """A cylindrical vessel volume with constant thickness. + + Arguments: + height (float): height of the vessel. + inner_radius (float): the inner radius of the vessel. + thickness (float): thickness of the vessel + stp_filename (str, optional): defaults to + "CenterColumnShieldCylinder.stp". + stl_filename (str, optional): defaults to + "CenterColumnShieldCylinder.stl". + material_tag (str, optional): defaults to "center_column_shield_mat". + """ + + def __init__( + self, + height, + inner_radius, + thickness, + stp_filename="CenterColumnShieldCylinder.stp", + stl_filename="CenterColumnShieldCylinder.stl", + material_tag="center_column_shield_mat", + **kwargs + ): + self.height = height + self.inner_radius = inner_radius + self.thickness = thickness + super().__init__( + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + @property + def height(self): + return self._height + + @height.setter + def height(self, height): + self._height = height + + @property + def inner_radius(self): + return self._inner_radius + + @inner_radius.setter + def inner_radius(self, inner_radius): + self._inner_radius = inner_radius + + def find_points(self): + """Finds the XZ points joined by straight connections that describe the + 2D profile of the vessel shape.""" + thickness = self.thickness + inner_radius = self.inner_radius + height = self.height + + inner_points = [ + (0, height / 2), + (inner_radius, height / 2), + (inner_radius, -height / 2), + (0, -height / 2), + ] + + outer_points = [ + (0, height / 2 + thickness), + (inner_radius + thickness, height / 2 + thickness), + (inner_radius + thickness, -(height / 2 + thickness)), + (0, -(height / 2 + thickness)), + ] + self.points = inner_points + outer_points[::-1] diff --git a/paramak/parametric_neutronics/__init__.py b/paramak/parametric_neutronics/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/paramak/parametric_neutronics/neutronics_model_from_reactor.py b/paramak/parametric_neutronics/neutronics_model_from_reactor.py new file mode 100644 index 000000000..c52d0ae41 --- /dev/null +++ b/paramak/parametric_neutronics/neutronics_model_from_reactor.py @@ -0,0 +1,615 @@ + +import json +import os +import warnings +from collections import defaultdict +from pathlib import Path + +import matplotlib.pyplot as plt + +try: + import openmc +except BaseException: + warnings.warn('OpenMC not found, NeutronicsModelFromReactor.simulate \ + method not available', UserWarning) + +try: + import neutronics_material_maker as nmm +except BaseException: + warnings.warn("neutronics_material_maker not found, \ + NeutronicsModelFromReactor.materials can't accept strings or \ + neutronics_material_maker objects", UserWarning) + + +class NeutronicsModelFromReactor(): + """Creates a neuronics model of the provided reactor geometry with assigned + materials, plasma source and neutronics tallies. There are two methods + available for producing the imprinted and merged h5m geometry (PPP or + Trelis) and one method of producing non imprinted and non merged geometry + (PyMoab). make_watertight is also used to seal the DAGMC geoemtry. If using + the Trelis option you must have the make_faceteted_neutronics_model.py in + the same directory as your Python script. Further details on imprinting + and merging are available on the DAGMC homepage + https://svalinn.github.io/DAGMC/usersguide/trelis_basics.html + The Parallel-PreProcessor is an open-source tool available + https://github.com/ukaea/parallel-preprocessor and can be used in + conjunction with the OCC_faceter + (https://github.com/makeclean/occ_faceter) to create imprinted and + merged geometry while Trelis (also known as Cubit) is available from + the CoreForm website https://www.coreform.com/ + + Arguments: + reactor (paramak.Reactor): The reactor object to convert to a + neutronics model. e.g. reactor=paramak.BallReactor() or + reactor=paramak.SubmersionReactor() . + materials (dict): Where the dictionary keys are the material tag + and the dictionary values are either a string, openmc.Material, + neutronics-material-maker.Material or + neutronics-material-maker.MultiMaterial. All components within the + Reactor() object must be accounted for. Material tags required + for the reactor can be obtained with Reactor().material_tags. + cell_tallies (list of strings): the cell based tallies to calculate, + options include TBR, heating and flux + mesh_tally_2D (list of strings): the mesh based tallies to calculate, + options include tritium_production, heating and flux + fusion_power (float): the power in watts emitted by the fusion + reaction recalling that each DT fusion reaction emitts 17.6 MeV or + 2.819831e-12 Joules + simulation_batches (int): the number of batch to simulate. + simulation_particles_per_batch: (int): particles per batch. + source (openmc.Source()): the particle source to use during the + OpenMC simulation. + merge_tolerance (float): the tolerance to use when merging surfaces. + Defaults to 1e-4. + faceting_tolerance (float): the tolerance to use when faceting surfaces. + Defaults to 1e-1. + mesh_2D_resolution (tuple of ints): The mesh resolution in the height + and width directions. The larger the resolution the finer the mesh + and more computational intensity is required to converge each mesh + element. + """ + + def __init__( + self, + reactor, + materials, + source, + cell_tallies=None, + mesh_tally_2D=None, + fusion_power=1e9, + simulation_batches=100, + simulation_particles_per_batch=10000, + max_lost_particles=10, + faceting_tolerance=1e-1, + merge_tolerance=1e-4, + mesh_2D_resolution=(400, 400) + ): + + self.reactor = reactor + self.materials = materials + self.source = source + self.cell_tallies = cell_tallies + self.mesh_tally_2D = mesh_tally_2D + self.simulation_batches = simulation_batches + self.simulation_particles_per_batch = simulation_particles_per_batch + self.max_lost_particles = max_lost_particles + self.faceting_tolerance = faceting_tolerance + self.merge_tolerance = merge_tolerance + self.mesh_2D_resolution = mesh_2D_resolution + self.model = None + self.fusion_power = fusion_power + + # Only 360 degree models are supported for now as reflecting surfaces + # are needed for sector models and they are not currently supported + if reactor.rotation_angle != 360: + reactor.rotation_angle = 360 + print('remaking reactor as it was not set to 360 degrees') + reactor.solid + # TODO make use of reactor.create_solids() here + + @property + def faceting_tolerance(self): + return self._faceting_tolerance + + @faceting_tolerance.setter + def faceting_tolerance(self, value): + if not isinstance(value, (int, float)): + raise ValueError( + "NeutronicsModelFromReactor.faceting_tolerance should be a\ + number (floats or ints are accepted)") + if value < 0: + raise ValueError( + "NeutronicsModelFromReactor.faceting_tolerance should be a\ + positive number") + self._faceting_tolerance = value + + @property + def merge_tolerance(self): + return self._merge_tolerance + + @merge_tolerance.setter + def merge_tolerance(self, value): + if not isinstance(value, (int, float)): + raise ValueError( + "NeutronicsModelFromReactor.merge_tolerance should be a\ + number (floats or ints are accepted)") + if value < 0: + raise ValueError( + "NeutronicsModelFromReactor.merge_tolerance should be a\ + positive number") + self._merge_tolerance = value + + @property + def cell_tallies(self): + return self._cell_tallies + + @cell_tallies.setter + def cell_tallies(self, value): + if value is not None: + if not isinstance(value, list): + raise ValueError( + "NeutronicsModelFromReactor.cell_tallies should be a\ + list") + output_options = ['TBR', 'heating', 'flux', 'fast flux', 'dose'] + for entry in value: + if entry not in output_options: + raise ValueError( + "NeutronicsModelFromReactor.cell_tallies argument", + entry, + "not allowed, the following options are supported", + output_options) + self._cell_tallies = value + + @property + def mesh_tally_2D(self): + return self._mesh_tally_2D + + @mesh_tally_2D.setter + def mesh_tally_2D(self, value): + if value is not None: + if not isinstance(value, list): + raise ValueError( + "NeutronicsModelFromReactor.mesh_tally_2D should be a\ + list") + output_options = ['tritium_production', 'heating', 'flux', + 'fast flux', 'dose'] + for entry in value: + if entry not in output_options: + raise ValueError( + "NeutronicsModelFromReactor.mesh_tally_2D argument", + entry, + "not allowed, the following options are supported", + output_options) + self._mesh_tally_2D = value + + @property + def materials(self): + return self._materials + + @materials.setter + def materials(self, value): + if not isinstance(value, dict): + raise ValueError("NeutronicsModelFromReactor.materials should be a\ + dictionary") + self._materials = value + + @property + def simulation_batches(self): + return self._simulation_batches + + @simulation_batches.setter + def simulation_batches(self, value): + if isinstance(value, float): + value = int(value) + if not isinstance(value, int): + raise ValueError( + "NeutronicsModelFromReactor.simulation_batches should be an int") + self._simulation_batches = value + + @property + def simulation_particles_per_batch(self): + return self._simulation_particles_per_batch + + @simulation_particles_per_batch.setter + def simulation_particles_per_batch(self, value): + if isinstance(value, float): + value = int(value) + if not isinstance(value, int): + raise ValueError( + "NeutronicsModelFromReactor.simulation_particles_per_batch\ + should be an int") + self._simulation_particles_per_batch = value + + def create_materials(self): + # checks all the required materials are present + for reactor_material in self.reactor.material_tags: + if reactor_material not in self.materials.keys(): + raise ValueError( + "material included by the reactor model has not \ + been added", reactor_material) + + # checks that no extra materials we added + for reactor_material in self.materials.keys(): + if reactor_material not in self.reactor.material_tags: + raise ValueError( + "material has been added that is not needed for this \ + reactor model", reactor_material) + + openmc_materials = {} + for material_tag, material_entry in self.materials.items(): + if isinstance(material_entry, str): + material = nmm.Material( + material_entry, material_tag=material_tag) + openmc_materials[material_tag] = material.openmc_material + elif isinstance(material_entry, openmc.Material): + # sets the material name in the event that it had not been set + material_entry.name = material_tag + openmc_materials[material_tag] = material_entry + elif isinstance(material_entry, (nmm.Material, nmm.MultiMaterial)): + # sets the material tag in the event that it had not been set + material_entry.material_tag = material_tag + openmc_materials[material_tag] = material_entry.openmc_material + else: + raise ValueError("materials must be either a str, \ + openmc.Material, nmm.MultiMaterial or nmm.Material object \ + not a ", type(material_entry), material_entry) + + self.openmc_materials = openmc_materials + + self.mats = openmc.Materials(list(self.openmc_materials.values())) + + return self.mats + + def create_neutronics_geometry(self, method=None): + """Produces a dagmc.h5m neutronics file compatable with DAGMC + simulations. + + Arguments: + method: (str): The method to use when making the imprinted and + merged geometry. Options are "ppp", "trelis", "pymoab". + Defaults to None. + """ + + os.system('rm dagmc_not_watertight.h5m') + os.system('rm dagmc.h5m') + + if method not in ['ppp', 'trelis', 'pymoab']: + raise ValueError( + "the method using in create_neutronics_geometry \ + should be either ppp or trelis not", method) + + if method == 'ppp': + + self.reactor.export_stp() + self.reactor.export_neutronics_description() + # as the installer connects to the system python not the conda + # python this full path is needed for now + if os.system( + '/usr/bin/python3 /usr/bin/geomPipeline.py manifest.json') != 0: + raise ValueError( + "geomPipeline.py failed, check PPP is installed") + + # TODO allow tolerance to be user controlled + if os.system( + 'occ_faceter manifest_processed/manifest_processed.brep') != 0: + raise ValueError( + "occ_faceter failed, check occ_faceter is install and the \ + occ_faceter/bin folder is in the path directory") + self._make_watertight() + + elif method == 'trelis': + self.reactor.export_stp() + self.reactor.export_neutronics_description() + + if not Path("make_faceteted_neutronics_model.py").is_file(): + raise ValueError("The make_faceteted_neutronics_model.py was \ + not found in the directory") + os.system("trelis -batch -nographics make_faceteted_neutronics_model.py \"faceting_tolerance='" + + str(self.faceting_tolerance) + "'\" \"merge_tolerance='" + str(self.merge_tolerance) + "'\"") + + if not Path("dagmc_not_watertight.h5m").is_file(): + raise ValueError("The dagmc_not_watertight.h5m was not found \ + in the directory, the Trelis stage has failed") + self._make_watertight() + + elif method == 'pymoab': + + self.reactor.export_h5m( + filename='dagmc.h5m', + tolerance=self.faceting_tolerance + ) + + print('neutronics model saved as dagmc.h5m') + + def _make_watertight(self): + """Runs the DAGMC make_watertight script thatt seals the facetets of + the geometry""" + + if not Path("dagmc_not_watertight.h5m").is_file(): + raise ValueError( + "Failed to create a dagmc_not_watertight.h5m file") + + if os.system( + "make_watertight dagmc_not_watertight.h5m -o dagmc.h5m") != 0: + raise ValueError( + "make_watertight failed, check DAGMC is install and the \ + DAGMC/bin folder is in the path directory") + + def create_neutronics_model(self, method=None): + """Uses OpenMC python API to make a neutronics model, including tallies + (cell_tallies and mesh_tally_2D), simulation settings (batches, + particles per batch). + + Arguments: + method: (str): The method to use when making the imprinted and + merged geometry. Options are "ppp", "trelis", "pymoab". + Defaults to None. + """ + + self.create_materials() + + self.create_neutronics_geometry(method=method) + + # this is the underlying geometry container that is filled with the + # faceteted DGAMC CAD model + self.universe = openmc.Universe() + geom = openmc.Geometry(self.universe) + + # settings for the number of neutrons to simulate + settings = openmc.Settings() + settings.batches = self.simulation_batches + settings.inactive = 0 + settings.particles = self.simulation_particles_per_batch + settings.run_mode = "fixed source" + settings.dagmc = True + settings.photon_transport = True + settings.source = self.source + settings.max_lost_particles = self.max_lost_particles + + # details about what neutrons interactions to keep track of (tally) + tallies = openmc.Tallies() + + if self.mesh_tally_2D is not None: + + # Create mesh which will be used for tally + mesh_xz = openmc.RegularMesh() + mesh_xz.dimension = [ + self.mesh_2D_resolution[1], + 1, + self.mesh_2D_resolution[0]] + mesh_xz.lower_left = [-self.reactor.largest_dimension, - + 1, -self.reactor.largest_dimension] + mesh_xz.upper_right = [ + self.reactor.largest_dimension, + 1, + self.reactor.largest_dimension] + + mesh_xy = openmc.RegularMesh() + mesh_xy.dimension = [ + self.mesh_2D_resolution[1], + self.mesh_2D_resolution[0], + 1] + mesh_xy.lower_left = [-self.reactor.largest_dimension, - + self.reactor.largest_dimension, -1] + mesh_xy.upper_right = [ + self.reactor.largest_dimension, + self.reactor.largest_dimension, + 1] + + mesh_yz = openmc.RegularMesh() + mesh_yz.dimension = [1, + self.mesh_2D_resolution[1], + self.mesh_2D_resolution[0]] + mesh_yz.lower_left = [-1, -self.reactor.largest_dimension, - + self.reactor.largest_dimension] + mesh_yz.upper_right = [1, + self.reactor.largest_dimension, + self.reactor.largest_dimension] + + if 'tritium_production' in self.mesh_tally_2D: + mesh_filter = openmc.MeshFilter(mesh_xz) + mesh_tally = openmc.Tally( + name='tritium_production_on_2D_mesh_xz') + mesh_tally.filters = [mesh_filter] + mesh_tally.scores = ['(n,Xt)'] + tallies.append(mesh_tally) + + mesh_filter = openmc.MeshFilter(mesh_xy) + mesh_tally = openmc.Tally( + name='tritium_production_on_2D_mesh_xy') + mesh_tally.filters = [mesh_filter] + mesh_tally.scores = ['(n,Xt)'] + tallies.append(mesh_tally) + + mesh_filter = openmc.MeshFilter(mesh_yz) + mesh_tally = openmc.Tally( + name='tritium_production_on_2D_mesh_yz') + mesh_tally.filters = [mesh_filter] + mesh_tally.scores = ['(n,Xt)'] + tallies.append(mesh_tally) + + if 'heating' in self.mesh_tally_2D: + mesh_filter = openmc.MeshFilter(mesh_xz) + mesh_tally = openmc.Tally(name='heating_on_2D_mesh_xz') + mesh_tally.filters = [mesh_filter] + mesh_tally.scores = ['heating'] + tallies.append(mesh_tally) + + mesh_filter = openmc.MeshFilter(mesh_xy) + mesh_tally = openmc.Tally(name='heating_on_2D_mesh_xy') + mesh_tally.filters = [mesh_filter] + mesh_tally.scores = ['heating'] + tallies.append(mesh_tally) + + mesh_filter = openmc.MeshFilter(mesh_yz) + mesh_tally = openmc.Tally(name='heating_on_2D_mesh_yz') + mesh_tally.filters = [mesh_filter] + mesh_tally.scores = ['heating'] + tallies.append(mesh_tally) + + if 'flux' in self.mesh_tally_2D: + mesh_filter = openmc.MeshFilter(mesh_xz) + mesh_tally = openmc.Tally(name='flux_on_2D_mesh_xz') + mesh_tally.filters = [mesh_filter] + mesh_tally.scores = ['flux'] + tallies.append(mesh_tally) + + mesh_filter = openmc.MeshFilter(mesh_xy) + mesh_tally = openmc.Tally(name='flux_on_2D_mesh_xy') + mesh_tally.filters = [mesh_filter] + mesh_tally.scores = ['flux'] + tallies.append(mesh_tally) + + mesh_filter = openmc.MeshFilter(mesh_yz) + mesh_tally = openmc.Tally(name='flux_on_2D_mesh_yz') + mesh_tally.filters = [mesh_filter] + mesh_tally.scores = ['flux'] + tallies.append(mesh_tally) + + if self.cell_tallies is not None: + + if 'TBR' in self.cell_tallies: + blanket_mat = self.openmc_materials['blanket_mat'] + material_filter = openmc.MaterialFilter(blanket_mat) + tally = openmc.Tally(name="TBR") + tally.filters = [material_filter] + tally.scores = ["(n,Xt)"] # where X is a wild card + tallies.append(tally) + + if 'heating' in self.cell_tallies: + for key, value in self.openmc_materials.items(): + if key != 'DT_plasma': + material_filter = openmc.MaterialFilter(value) + tally = openmc.Tally(name=key + "_heating") + tally.filters = [material_filter] + tally.scores = ["heating"] + tallies.append(tally) + + if 'flux' in self.cell_tallies: + for key, value in self.openmc_materials.items(): + if key != 'DT_plasma': + material_filter = openmc.MaterialFilter(value) + tally = openmc.Tally(name=key + "_flux") + tally.filters = [material_filter] + tally.scores = ["flux"] + tallies.append(tally) + + # make the model from gemonetry, materials, settings and tallies + self.model = openmc.model.Model(geom, self.mats, settings, tallies) + + def simulate(self, verbose=True, method=None): + """Run the OpenMC simulation. Deletes exisiting simulation output + (summary.h5) if files exists. + + Arguments: + verbose (Boolean, optional): Print the output from OpenMC (true) + to the terminal and don't print the OpenMC output (false). + Defaults to True. + method (str): The method to use when making the imprinted and + merged geometry. Options are "ppp", "trelis", "pymoab". + Defaults to pymoab. + + Returns: + dict: the simulation output filename + """ + + self.create_neutronics_model(method=method) + + # Deletes summary.h5m if it already exists. + # This avoids permission problems when trying to overwrite the file + os.system('rm summary.h5') + + self.output_filename = self.model.run(output=verbose) + self.results = self.get_results() + + return self.output_filename + + def get_results(self): + """Reads the output file from the neutronics simulation + and prints the TBR tally result to screen + + Returns: + dict: a dictionary of the simulation results + """ + + # open the results file + sp = openmc.StatePoint(self.output_filename) + + results = defaultdict(dict) + + # access the tallies + for key, tally in sp.tallies.items(): + + if tally.name == 'TBR': + + df = tally.get_pandas_dataframe() + tally_result = df["mean"].sum() + tally_std_dev = df['std. dev.'].sum() + results[tally.name] = { + 'result': tally_result, + 'std. dev.': tally_std_dev, + } + + if tally.name.endswith('heating'): + + df = tally.get_pandas_dataframe() + tally_result = df["mean"].sum() + tally_std_dev = df['std. dev.'].sum() + results[tally.name]['MeV per source particle'] = { + 'result': tally_result / 1e6, + 'std. dev.': tally_std_dev / 1e6, + } + results[tally.name]['Watts'] = { + 'result': tally_result * 1.602176487e-19 * (self.fusion_power / ((17.58 * 1e6) / 6.2415090744e18)), + 'std. dev.': tally_std_dev * 1.602176487e-19 * (self.fusion_power / ((17.58 * 1e6) / 6.2415090744e18)), + } + + if tally.name.endswith('flux'): + + df = tally.get_pandas_dataframe() + tally_result = df["mean"].sum() + tally_std_dev = df['std. dev.'].sum() + results[tally.name]['Flux per source particle'] = { + 'result': tally_result, + 'std. dev.': tally_std_dev, + } + + if tally.name.startswith('tritium_production_on_2D_mesh'): + + my_tally = sp.get_tally(name=tally.name) + my_slice = my_tally.get_slice(scores=['(n,Xt)']) + + my_slice.mean.shape = self.mesh_2D_resolution + + fig = plt.subplot() + fig.imshow(my_slice.mean).get_figure().savefig( + 'tritium_production_on_2D_mesh' + tally.name[-3:], dpi=300) + fig.clear() + + if tally.name.startswith('heating_on_2D_mesh'): + + my_tally = sp.get_tally(name=tally.name) + my_slice = my_tally.get_slice(scores=['heating']) + + my_slice.mean.shape = self.mesh_2D_resolution + + fig = plt.subplot() + fig.imshow(my_slice.mean).get_figure().savefig( + 'heating_on_2D_mesh' + tally.name[-3:], dpi=300) + fig.clear() + + if tally.name.startswith('flux_on_2D_mesh'): + + my_tally = sp.get_tally(name=tally.name) + my_slice = my_tally.get_slice(scores=['flux']) + + my_slice.mean.shape = self.mesh_2D_resolution + + fig = plt.subplot() + fig.imshow(my_slice.mean).get_figure().savefig( + 'flux_on_2D_mesh' + tally.name[-3:], dpi=300) + fig.clear() + + self.results = json.dumps(results, indent=4, sort_keys=True) + + return results diff --git a/paramak/parametric_reactors/__init__.py b/paramak/parametric_reactors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/paramak/parametric_reactors/ball_reactor.py b/paramak/parametric_reactors/ball_reactor.py new file mode 100644 index 000000000..6cae0fb92 --- /dev/null +++ b/paramak/parametric_reactors/ball_reactor.py @@ -0,0 +1,525 @@ + +import warnings + +import paramak + + +class BallReactor(paramak.Reactor): + """Creates geometry for a simple ball reactor including a plasma, + cylindrical center column shielding, square toroidal field coils. + There is no inboard breeder blanket on this ball reactor like + most spherical reactors. + + Arguments: + inner_bore_radial_thickness (float): the radial thickness of the + inner bore (cm) + inboard_tf_leg_radial_thickness (float): the radial thickness of the + inner leg of the toroidal field coils (cm) + center_column_shield_radial_thickness (float): the radial thickness of + the center column shield (cm) + divertor_radial_thickness (float): the radial thickness of the divertor + (cm), this fills the gap between the center column shield and + blanket + inner_plasma_gap_radial_thickness (float): the radial thickness of the + inboard gap between the plasma and the center column shield (cm) + plasma_radial_thickness (float): the radial thickness of the plasma + outer_plasma_gap_radial_thickness (float): the radial thickness of the + outboard gap between the plasma and firstwall (cm) + firstwall_radial_thickness (float): the radial thickness of the first + wall (cm) + blanket_radial_thickness (float): the radial thickness of the blanket + (cm) + blanket_rear_wall_radial_thickness (float): the radial thickness of the + rear wall of the blanket (cm) + elongation (float): the elongation of the plasma + triangularity (float): the triangularity of the plasma + plasma_gap_vertical_thickness (float): the vertical thickness of the + gap between the plasma and firstwall (cm). If left as None then the + outer_plasma_gap_radial_thickness is used. Defaults to None. + number_of_tf_coils (int, optional): the number of tf coils + pf_coil_to_rear_blanket_radial_gap (float, optional): the radial + distance between the rear blanket and the closest poloidal field + coil. Defaults to None. + pf_coil_radial_thicknesses (list of floats, optional): the radial + thickness of each poloidal field coil. Defaults to None. + pf_coil_vertical_thicknesses (list of floats, optional): the vertical + thickness of each poloidal field coil. Defaults to None. + pf_coil_to_tf_coil_radial_gap (float, optional): the radial distance + between the rear of the poloidal field coil and the toroidal field + coil. Defaults to None. + pf_coil_case_thickness (float or list of floats, optional): the + thickness(s) to use in both the radial and vertical direction for + the casing around the pf coils. If float then the single value will + be applied to all pf coils. If list then each value will be applied + to the pf coils one by one. To have no casing set to 0. Defaults to + 10. + outboard_tf_coil_radial_thickness (float, optional): the radial + thickness of the toroidal field coil. Defaults to None. + outboard_tf_coil_poloidal_thickness (float, optional): the poloidal + thickness of the toroidal field coil. Defaults to None. + divertor_position (str, optional): the position of the divertor, + "upper", "lower" or "both". Defaults to "both". + rotation_angle (float): the angle of the sector that is desired. + Defaults to 360.0. + """ + + def __init__( + self, + inner_bore_radial_thickness, + inboard_tf_leg_radial_thickness, + center_column_shield_radial_thickness, + divertor_radial_thickness, + inner_plasma_gap_radial_thickness, + plasma_radial_thickness, + outer_plasma_gap_radial_thickness, + firstwall_radial_thickness, + blanket_radial_thickness, + blanket_rear_wall_radial_thickness, + elongation, + triangularity, + plasma_gap_vertical_thickness=None, + number_of_tf_coils=12, + pf_coil_to_rear_blanket_radial_gap=None, + pf_coil_radial_thicknesses=None, + pf_coil_vertical_thicknesses=None, + pf_coil_to_tf_coil_radial_gap=None, + pf_coil_case_thickness=10, + outboard_tf_coil_radial_thickness=None, + outboard_tf_coil_poloidal_thickness=None, + divertor_position="both", + rotation_angle=360.0, + ): + + super().__init__([]) + + self.inner_bore_radial_thickness = inner_bore_radial_thickness + self.inboard_tf_leg_radial_thickness = inboard_tf_leg_radial_thickness + self.center_column_shield_radial_thickness = \ + center_column_shield_radial_thickness + self.divertor_radial_thickness = divertor_radial_thickness + self.inner_plasma_gap_radial_thickness = \ + inner_plasma_gap_radial_thickness + self.plasma_radial_thickness = plasma_radial_thickness + self.outer_plasma_gap_radial_thickness = \ + outer_plasma_gap_radial_thickness + self.firstwall_radial_thickness = firstwall_radial_thickness + self.blanket_radial_thickness = blanket_radial_thickness + self.blanket_rear_wall_radial_thickness = \ + blanket_rear_wall_radial_thickness + self.pf_coil_to_rear_blanket_radial_gap = \ + pf_coil_to_rear_blanket_radial_gap + self.pf_coil_radial_thicknesses = pf_coil_radial_thicknesses + self.pf_coil_vertical_thicknesses = pf_coil_vertical_thicknesses + self.pf_coil_to_tf_coil_radial_gap = pf_coil_to_tf_coil_radial_gap + self.pf_coil_case_thickness = pf_coil_case_thickness + self.outboard_tf_coil_radial_thickness = \ + outboard_tf_coil_radial_thickness + self.outboard_tf_coil_poloidal_thickness = \ + outboard_tf_coil_poloidal_thickness + self.divertor_position = divertor_position + + self.plasma_gap_vertical_thickness = plasma_gap_vertical_thickness + if self.plasma_gap_vertical_thickness is None: + self.plasma_gap_vertical_thickness = \ + self.outer_plasma_gap_radial_thickness + # sets major radius and minor radius from equatorial_points to allow a + # radial build + # this helps avoid the plasma overlapping the center column and other + # components + + inner_equatorial_point = ( + inner_bore_radial_thickness + + inboard_tf_leg_radial_thickness + + center_column_shield_radial_thickness + + inner_plasma_gap_radial_thickness + ) + outer_equatorial_point = \ + inner_equatorial_point + plasma_radial_thickness + self.major_radius = \ + (outer_equatorial_point + inner_equatorial_point) / 2 + self.minor_radius = self.major_radius - inner_equatorial_point + + self.elongation = elongation + self.triangularity = triangularity + + self.number_of_tf_coils = number_of_tf_coils + self.rotation_angle = rotation_angle + + self.offset_from_plasma = [ + self.major_radius - self.minor_radius, + self.plasma_gap_vertical_thickness, + self.outer_plasma_gap_radial_thickness, + self.plasma_gap_vertical_thickness, + self.major_radius - self.minor_radius] + + @property + def pf_coil_radial_thicknesses(self): + return self._pf_coil_radial_thicknesses + + @pf_coil_radial_thicknesses.setter + def pf_coil_radial_thicknesses(self, value): + if not isinstance(value, list) and value is not None: + raise ValueError("pf_coil_radial_thicknesses must be a list") + self._pf_coil_radial_thicknesses = value + + @property + def pf_coil_vertical_thicknesses(self): + return self._pf_coil_vertical_thicknesses + + @pf_coil_vertical_thicknesses.setter + def pf_coil_vertical_thicknesses(self, value): + if not isinstance(value, list) and value is not None: + raise ValueError("pf_coil_vertical_thicknesses must be a list") + self._pf_coil_vertical_thicknesses = value + + @property + def divertor_position(self): + return self._divertor_position + + @divertor_position.setter + def divertor_position(self, value): + acceptable_values = ["upper", "lower", "both"] + if value in acceptable_values: + self._divertor_position = value + else: + msg = "divertor_position must be 'upper', 'lower' or 'both'" + raise ValueError(msg) + + def create_solids(self): + """Creates a list of paramak.Shape for components and saves it in + self.shapes_and_components + """ + shapes_and_components = [] + + self._rotation_angle_check() + shapes_and_components.append(self._make_plasma()) + self._make_radial_build() + self._make_vertical_build() + shapes_and_components.append(self._make_inboard_tf_coils()) + shapes_and_components.append(self._make_center_column_shield()) + shapes_and_components += self._make_blankets_layers() + shapes_and_components.append(self._make_divertor()) + shapes_and_components += self._make_pf_coils() + shapes_and_components += self._make_tf_coils() + + self.shapes_and_components = shapes_and_components + + def _rotation_angle_check(self): + + if self.rotation_angle == 360: + msg = "360 degree rotation may result " + \ + "in a Standard_ConstructionError or AttributeError" + warnings.warn(msg, UserWarning) + + def _make_plasma(self): + + plasma = paramak.Plasma( + major_radius=self.major_radius, + minor_radius=self.minor_radius, + elongation=self.elongation, + triangularity=self.triangularity, + rotation_angle=self.rotation_angle, + ) + + self._plasma = plasma + return self._plasma + + def _make_radial_build(self): + + # this is the radial build sequence, where one component stops and + # another starts + + self._inner_bore_start_radius = 0 + self._inner_bore_end_radius = ( + self._inner_bore_start_radius + self.inner_bore_radial_thickness + ) + + self._inboard_tf_coils_start_radius = self._inner_bore_end_radius + self._inboard_tf_coils_end_radius = ( + self._inboard_tf_coils_start_radius + + self.inboard_tf_leg_radial_thickness) + + self._center_column_shield_start_radius = \ + self._inboard_tf_coils_end_radius + self._center_column_shield_end_radius = ( + self._center_column_shield_start_radius + + self.center_column_shield_radial_thickness + ) + + self._divertor_start_radius = self._center_column_shield_end_radius + self._divertor_end_radius = ( + self._center_column_shield_end_radius + + self.divertor_radial_thickness) + + self._firstwall_start_radius = ( + self._center_column_shield_end_radius + + self.inner_plasma_gap_radial_thickness + + self.plasma_radial_thickness + + self.outer_plasma_gap_radial_thickness + ) + self._firstwall_end_radius = self._firstwall_start_radius + \ + self.firstwall_radial_thickness + + self._blanket_start_radius = self._firstwall_end_radius + self._blanket_end_radius = \ + self._blanket_start_radius + self.blanket_radial_thickness + + self._blanket_rear_wall_start_radius = self._blanket_end_radius + self._blanket_rear_wall_end_radius = ( + self._blanket_rear_wall_start_radius + + self.blanket_rear_wall_radial_thickness) + + def _make_vertical_build(self): + + # this is the vertical build sequence, components build on each other + # in a similar manner to the radial build + + self._firstwall_start_height = ( + self._plasma.high_point[1] + self.plasma_gap_vertical_thickness + ) + self._firstwall_end_height = self._firstwall_start_height + \ + self.firstwall_radial_thickness + + self._blanket_start_height = self._firstwall_end_height + self._blanket_end_height = \ + self._blanket_start_height + self.blanket_radial_thickness + + self._blanket_rear_wall_start_height = self._blanket_end_height + self._blanket_rear_wall_end_height = ( + self._blanket_rear_wall_start_height + + self.blanket_rear_wall_radial_thickness) + + self._tf_coil_height = self._blanket_rear_wall_end_height + self._center_column_shield_height = \ + self._blanket_rear_wall_end_height * 2 + + if (self.pf_coil_vertical_thicknesses, + self.pf_coil_radial_thicknesses, + self.pf_coil_to_rear_blanket_radial_gap) != (None, None, None): + self._number_of_pf_coils = len(self.pf_coil_vertical_thicknesses) + + y_position_step = (2 * ( + self._blanket_rear_wall_end_height + + self.pf_coil_to_rear_blanket_radial_gap + ) + ) / (self._number_of_pf_coils + 1) + + if not isinstance(self.pf_coil_case_thickness, list): + self.pf_coil_case_thickness = [ + self.pf_coil_case_thickness] * self._number_of_pf_coils + + self._pf_coils_xy_values = [] + # adds in coils with equal spacing strategy, should be updated to + # allow user positions + for i in range(self._number_of_pf_coils): + y_value = ( + self._blanket_rear_wall_end_height + + self.pf_coil_to_rear_blanket_radial_gap + - y_position_step * (i + 1) + ) + x_value = ( + self._blanket_rear_wall_end_radius + + self.pf_coil_to_rear_blanket_radial_gap + + 0.5 * self.pf_coil_radial_thicknesses[i] + + self.pf_coil_case_thickness[i] + ) + self._pf_coils_xy_values.append((x_value, y_value)) + + self._pf_coil_start_radius = ( + self._blanket_rear_wall_end_radius + + self.pf_coil_to_rear_blanket_radial_gap) + + self._pf_coil_end_radius = self._pf_coil_start_radius + \ + max(self.pf_coil_radial_thicknesses) + \ + max(self.pf_coil_case_thickness) * 2 + + if (self.pf_coil_to_tf_coil_radial_gap, + self.outboard_tf_coil_radial_thickness) != (None, None): + self._tf_coil_start_radius = ( + self._pf_coil_end_radius + + self.pf_coil_to_rear_blanket_radial_gap) + self._tf_coil_end_radius = ( + self._tf_coil_start_radius + + self.outboard_tf_coil_radial_thickness) + + def _make_inboard_tf_coils(self): + + self._inboard_tf_coils = paramak.CenterColumnShieldCylinder( + height=self._tf_coil_height * 2, + inner_radius=self._inboard_tf_coils_start_radius, + outer_radius=self._inboard_tf_coils_end_radius, + rotation_angle=self.rotation_angle, + # color=centre_column_color, + stp_filename="inboard_tf_coils.stp", + stl_filename="inboard_tf_coils.stl", + name="inboard_tf_coils", + material_tag="inboard_tf_coils_mat", + ) + return self._inboard_tf_coils + + def _make_center_column_shield(self): + + self._center_column_shield = paramak.CenterColumnShieldCylinder( + height=self._center_column_shield_height, + inner_radius=self._center_column_shield_start_radius, + outer_radius=self._center_column_shield_end_radius, + rotation_angle=self.rotation_angle, + # color=centre_column_color, + stp_filename="center_column_shield.stp", + stl_filename="center_column_shield.stl", + name="center_column_shield", + material_tag="center_column_shield_mat", + ) + return self._center_column_shield + + def _make_blankets_layers(self): + + self._center_column_cutter = paramak.CenterColumnShieldCylinder( + # extra 0.5 to ensure overlap, + height=self._center_column_shield_height * 1.5, + inner_radius=0, + outer_radius=self._center_column_shield_end_radius, + rotation_angle=360 + ) + + self._firstwall = paramak.BlanketFP( + plasma=self._plasma, + thickness=self.firstwall_radial_thickness, + offset_from_plasma=self.offset_from_plasma, + start_angle=-180, + stop_angle=180, + rotation_angle=self.rotation_angle, + material_tag="firstwall_mat", + stp_filename="firstwall.stp", + stl_filename="firstwall.stl", + cut=[self._center_column_cutter] + ) + + self._blanket = paramak.BlanketFP( + plasma=self._plasma, + thickness=self.blanket_radial_thickness, + offset_from_plasma=[e + self.firstwall_radial_thickness + for e in self.offset_from_plasma], + start_angle=-180, + stop_angle=180, + rotation_angle=self.rotation_angle, + material_tag="blanket_mat", + stp_filename="blanket.stp", + stl_filename="blanket.stl", + cut=[self._center_column_cutter]) + + self._blanket_rear_wall = paramak.BlanketFP( + plasma=self._plasma, + thickness=self.blanket_rear_wall_radial_thickness, + offset_from_plasma=[e + self.firstwall_radial_thickness + + self.blanket_radial_thickness + for e in self.offset_from_plasma], + start_angle=-180, + stop_angle=180, + rotation_angle=self.rotation_angle, + material_tag="blanket_rear_wall_mat", + stp_filename="blanket_rear_wall.stp", + stl_filename="blanket_rear_wall.stl", + cut=[self._center_column_cutter], + ) + + return [self._firstwall, self._blanket, self._blanket_rear_wall] + + def _make_divertor(self): + # # used as an intersect when making the divertor + self._blanket_fw_rear_wall_envelope = paramak.BlanketFP( + plasma=self._plasma, + thickness=self.firstwall_radial_thickness + + self.blanket_radial_thickness + + self.blanket_rear_wall_radial_thickness, + offset_from_plasma=self.offset_from_plasma, + start_angle=-180, + stop_angle=180, + rotation_angle=self.rotation_angle, + ) + + divertor_height = self._blanket_rear_wall_end_height * 2 + + divertor_height_top = divertor_height + divertor_height_bottom = -divertor_height + + if self.divertor_position == "lower": + divertor_height_top = 0 + elif self.divertor_position == "upper": + divertor_height_bottom = 0 + self._divertor = paramak.RotateStraightShape( + points=[ + (self._divertor_start_radius, divertor_height_bottom), + (self._divertor_end_radius, divertor_height_bottom), + (self._divertor_end_radius, divertor_height_top), + (self._divertor_start_radius, divertor_height_top) + ], + intersect=self._blanket_fw_rear_wall_envelope, + stp_filename="divertor.stp", + stl_filename="divertor.stl", + name="divertor", + material_tag="divertor_mat", + rotation_angle=self.rotation_angle + ) + + for component in [ + self._firstwall, + self._blanket, + self._blanket_rear_wall]: + component.cut.append(self._divertor) + + return self._divertor + + def _make_pf_coils(self): + list_of_components = [] + if (self.pf_coil_vertical_thicknesses, + self.pf_coil_radial_thicknesses, + self.pf_coil_to_rear_blanket_radial_gap) != (None, None, None): + + self._pf_coil = paramak.PoloidalFieldCoilSet( + heights=self.pf_coil_vertical_thicknesses, + widths=self.pf_coil_radial_thicknesses, + center_points=self._pf_coils_xy_values, + rotation_angle=self.rotation_angle, + stp_filename='pf_coils.stp', + stl_filename='pf_coils.stl', + name="pf_coil", + material_tag="pf_coil_mat", + ) + + self._pf_coils_casing = paramak.PoloidalFieldCoilCaseSetFC( + pf_coils=self._pf_coil, + casing_thicknesses=self.pf_coil_case_thickness, + rotation_angle=self.rotation_angle, + stp_filename='pf_coil_cases.stp', + stl_filename='pf_coil_cases.stl', + name="pf_coil_case", + material_tag="pf_coil_case_mat", + ) + + list_of_components = [self._pf_coils_casing, self._pf_coil] + return list_of_components + + def _make_tf_coils(self): + comp = [] + if (self.pf_coil_to_tf_coil_radial_gap, + self.outboard_tf_coil_radial_thickness) != (None, None): + + self._tf_coil = paramak.ToroidalFieldCoilRectangle( + with_inner_leg=False, + horizontal_start_point=( + self._inboard_tf_coils_start_radius, + self._tf_coil_height), + vertical_mid_point=( + self._tf_coil_start_radius, 0), + thickness=self.outboard_tf_coil_radial_thickness, + number_of_coils=self.number_of_tf_coils, + distance=self.outboard_tf_coil_poloidal_thickness, + stp_filename="tf_coil.stp", + name="tf_coil", + material_tag="tf_coil_mat", + stl_filename="tf_coil.stl", + rotation_angle=self.rotation_angle + ) + comp = [self._tf_coil] + return comp diff --git a/paramak/parametric_reactors/center_column_study_reactor.py b/paramak/parametric_reactors/center_column_study_reactor.py new file mode 100644 index 000000000..b1a665375 --- /dev/null +++ b/paramak/parametric_reactors/center_column_study_reactor.py @@ -0,0 +1,302 @@ + +import warnings + +import paramak + + +class CenterColumnStudyReactor(paramak.Reactor): + """Creates geometry for a simple reactor that is optimised for carrying + out parametric studies on the center column shield. Several aspects + such as outboard magnets are intentionally missing from this reactor + so that the model runs quickly and only includes components that have a + significant impact on the center column shielding. This allows the + neutronics simulations to run quickly and the column design space to be + explored efficiently. + + Arguments: + inner_bore_radial_thickness (float): the radial thickness of the + inner bore (cm) + inboard_tf_leg_radial_thickness (float): the radial thickness of + the inner leg of the toroidal field coils (cm) + center_column_shield_radial_thickness_mid (float): the radial thickness + of the center column shield at the mid point (cm) + center_column_shield_radial_thickness_upper (float): the radial + thickness of the center column shield at the upper point (cm) + inboard_firstwall_radial_thickness (float): the radial thickness + of the inboard firstwall (cm) + inner_plasma_gap_radial_thickness (float): the radial thickness of + the inboard gap between the plasma and the center column shield + (cm) + plasma_radial_thickness (float): the radial thickness of the plasma + (cm) + outer_plasma_gap_radial_thickness (float): the radial thickness of + the outboard gap between the plasma and the first wall (cm) + elongation (float): the elongation of the plasma + triangularity (float): the triangularity of the plasma + center_column_arc_vertical_thickness (float): height of the outer + hyperbolic profile of the center column shield. + plasma_gap_vertical_thickness (float): the vertical thickness of + the upper gap between the plasma and the blanket (cm) + rotation_angle (float): the angle of the sector that is desired. + Defaults to 360.0. + """ + + def __init__( + self, + inner_bore_radial_thickness, + inboard_tf_leg_radial_thickness, + center_column_shield_radial_thickness_mid, + center_column_shield_radial_thickness_upper, + inboard_firstwall_radial_thickness, + divertor_radial_thickness, + inner_plasma_gap_radial_thickness, + plasma_radial_thickness, + outer_plasma_gap_radial_thickness, + center_column_arc_vertical_thickness, + elongation, + triangularity, + plasma_gap_vertical_thickness, + rotation_angle=360.0, + ): + + super().__init__([]) + + self.inner_bore_radial_thickness = inner_bore_radial_thickness + self.inboard_tf_leg_radial_thickness = inboard_tf_leg_radial_thickness + self.center_column_shield_radial_thickness_mid = \ + center_column_shield_radial_thickness_mid + self.center_column_shield_radial_thickness_upper = \ + center_column_shield_radial_thickness_upper + self.inboard_firstwall_radial_thickness = \ + inboard_firstwall_radial_thickness + self.divertor_radial_thickness = divertor_radial_thickness + self.inner_plasma_gap_radial_thickness = \ + inner_plasma_gap_radial_thickness + self.plasma_radial_thickness = plasma_radial_thickness + self.outer_plasma_gap_radial_thickness = \ + outer_plasma_gap_radial_thickness + self.plasma_gap_vertical_thickness = plasma_gap_vertical_thickness + self.center_column_arc_vertical_thickness = \ + center_column_arc_vertical_thickness + self.rotation_angle = rotation_angle + self.elongation = elongation + self.triangularity = triangularity + + # sets major radius and minor radius from equatorial_points to allow a + # radial build this helps avoid the plasma overlapping the center + # column and other components + + inner_equatorial_point = ( + inner_bore_radial_thickness + + inboard_tf_leg_radial_thickness + + center_column_shield_radial_thickness_mid + + inner_plasma_gap_radial_thickness + ) + outer_equatorial_point = \ + inner_equatorial_point + plasma_radial_thickness + self.major_radius = \ + (outer_equatorial_point + inner_equatorial_point) / 2 + self.minor_radius = self.major_radius - inner_equatorial_point + + def create_solids(self): + """Creates a 3d solids for each component. + + Returns: + A list of CadQuery solids: A list of 3D solid volumes + + """ + shapes_and_components = [] + + self._rotation_angle_check() + shapes_and_components.append(self._make_plasma()) + self._make_radial_build() + self._make_vertical_build() + shapes_and_components.append(self._make_inboard_tf_coils()) + shapes_and_components.append(self._make_center_column_shield()) + shapes_and_components.append(self._make_inboard_firstwall()) + shapes_and_components.append(self._make_outboard_blanket()) + shapes_and_components.append(self._make_divertor()) + + self.shapes_and_components = shapes_and_components + + def _rotation_angle_check(self): + + if self.rotation_angle == 360: + msg = "360 degree rotation may result " + \ + "in a Standard_ConstructionError or AttributeError" + warnings.warn(msg, UserWarning) + + def _make_plasma(self): + + plasma = paramak.Plasma( + major_radius=self.major_radius, + minor_radius=self.minor_radius, + elongation=self.elongation, + triangularity=self.triangularity, + rotation_angle=self.rotation_angle, + ) + + self._plasma = plasma + return plasma + + def _make_radial_build(self): + + # this is the radial build sequence, where one component stops and + # another starts + + self._inner_bore_start_radius = 0 + self._inner_bore_end_radius = self._inner_bore_start_radius + \ + self.inner_bore_radial_thickness + + self._inboard_tf_coils_start_radius = self._inner_bore_end_radius + self._inboard_tf_coils_end_radius = \ + self._inboard_tf_coils_start_radius + \ + self.inboard_tf_leg_radial_thickness + + self._center_column_shield_start_radius = \ + self._inboard_tf_coils_end_radius + self._center_column_shield_end_radius_upper = \ + self._center_column_shield_start_radius + \ + self.center_column_shield_radial_thickness_upper + self._center_column_shield_end_radius_mid = \ + self._center_column_shield_start_radius + \ + self.center_column_shield_radial_thickness_mid + + self._inboard_firstwall_start_radius = \ + self._center_column_shield_end_radius_upper + self._inboard_firstwall_end_radius = \ + self._inboard_firstwall_start_radius + \ + self.inboard_firstwall_radial_thickness + + self._divertor_start_radius = self._inboard_firstwall_end_radius + self._divertor_end_radius = self._divertor_start_radius + \ + self.divertor_radial_thickness + + self._inner_plasma_gap_start_radius = \ + self._center_column_shield_end_radius_mid + \ + self.inboard_firstwall_radial_thickness + + self._inner_plasma_gap_end_radius = \ + self._inner_plasma_gap_start_radius + \ + self.inner_plasma_gap_radial_thickness + + self._plasma_start_radius = self._inner_plasma_gap_end_radius + self._plasma_end_radius = \ + self._plasma_start_radius + \ + self.plasma_radial_thickness + + self._outer_plasma_gap_start_radius = self._plasma_end_radius + self._outer_plasma_gap_end_radius = \ + self._outer_plasma_gap_start_radius + \ + self.outer_plasma_gap_radial_thickness + + self._outboard_blanket_start_radius = self._outer_plasma_gap_end_radius + self._outboard_blanket_end_radius = \ + self._outboard_blanket_start_radius + 100. + + def _make_vertical_build(self): + + # this is the vertical build sequence, componets build on each other in + # a similar manner to the radial build + + self._plasma_to_blanket_gap_start_height = self._plasma.high_point[1] + self._plasma_to_blanket_gap_end_height = \ + self._plasma_to_blanket_gap_start_height + \ + self.plasma_gap_vertical_thickness + + self._blanket_start_height = self._plasma_to_blanket_gap_end_height + self._blanket_end_height = self._blanket_start_height + 100. + + self._center_column_shield_end_height = self._blanket_end_height + self._inboard_firstwall_end_height = self._blanket_end_height + + def _make_inboard_tf_coils(self): + + self._inboard_tf_coils = paramak.CenterColumnShieldCylinder( + height=self._blanket_end_height * 2, + inner_radius=self._inboard_tf_coils_start_radius, + outer_radius=self._inboard_tf_coils_end_radius, + rotation_angle=self.rotation_angle, + stp_filename="inboard_tf_coils.stp", + stl_filename="inboard_tf_coils.stl", + name="inboard_tf_coils", + material_tag="inboard_tf_coils_mat", + ) + return self._inboard_tf_coils + + def _make_center_column_shield(self): + + self._center_column_shield = \ + paramak.CenterColumnShieldFlatTopHyperbola( + height=self._center_column_shield_end_height * 2., + arc_height=self.center_column_arc_vertical_thickness, + inner_radius=self._center_column_shield_start_radius, + mid_radius=self._center_column_shield_end_radius_mid, + outer_radius=self._center_column_shield_end_radius_upper, + rotation_angle=self.rotation_angle) + return self._center_column_shield + + def _make_inboard_firstwall(self): + + self._inboard_firstwall = paramak.InboardFirstwallFCCS( + central_column_shield=self._center_column_shield, + thickness=self.inboard_firstwall_radial_thickness, + rotation_angle=self.rotation_angle) + return self._inboard_firstwall + + def _make_outboard_blanket(self): + + self._center_column_cutter = paramak.CenterColumnShieldCylinder( + # extra 1.5 to ensure overlap, + height=self._inboard_firstwall_end_height * 2.5, + inner_radius=0, + outer_radius=self._inboard_firstwall_end_radius, + rotation_angle=self.rotation_angle + ) + + self._blanket = paramak.BlanketFP( + plasma=self._plasma, + thickness=100., + offset_from_plasma=[ + self.inner_plasma_gap_radial_thickness, + self.plasma_gap_vertical_thickness, + self.outer_plasma_gap_radial_thickness, + self.plasma_gap_vertical_thickness, + self.inner_plasma_gap_radial_thickness], + start_angle=-180, + stop_angle=180, + rotation_angle=self.rotation_angle, + cut=[self._center_column_cutter] + ) + return self._blanket + + def _make_divertor(self): + self._blanket_enveloppe = paramak.BlanketFP( + plasma=self._plasma, + thickness=100., + offset_from_plasma=[ + self.inner_plasma_gap_radial_thickness, + self.plasma_gap_vertical_thickness, + self.outer_plasma_gap_radial_thickness, + self.plasma_gap_vertical_thickness, + self.inner_plasma_gap_radial_thickness], + start_angle=-180, + stop_angle=180, + rotation_angle=self.rotation_angle, + cut=[self._center_column_cutter] + ) + + self._divertor = paramak.CenterColumnShieldCylinder( + height=self._center_column_shield_end_height * + 2.5, # extra 0.5 to ensure overlap + inner_radius=self._divertor_start_radius, + outer_radius=self._divertor_end_radius, + rotation_angle=self.rotation_angle, + stp_filename="divertor.stp", + stl_filename="divertor.stl", + name="divertor", + material_tag="divertor_mat", + intersect=self._blanket_enveloppe, + ) + self._blanket.cut.append(self._divertor) + return self._divertor diff --git a/paramak/parametric_reactors/segmented_blanket_ball_reactor.py b/paramak/parametric_reactors/segmented_blanket_ball_reactor.py new file mode 100644 index 000000000..ea79b5ba9 --- /dev/null +++ b/paramak/parametric_reactors/segmented_blanket_ball_reactor.py @@ -0,0 +1,118 @@ + +import paramak +import numpy as np +import cadquery as cq + + +class SegmentedBlanketBallReactor(paramak.BallReactor): + """Creates geometry for a single ball reactor with a single divertor + including a plasma, cylindrical center column shielding, square toroidal + field coils. There is no inboard breeder blanket on this ball reactor like + most spherical reactors. + + Arguments: + gap_between_blankets (float): the distance between adjacent blanket + segments, + number_of_blanket_segments (int): the number of segments to divide the + blanket up into. This for a full 360 degrees rotation + blanket_fillet_radius (float): the fillet radius to apply to the + interface between the firstwall and th breeder zone. Set to 0 for + no fillet. Defaults to 10.0. + """ + + def __init__( + self, + gap_between_blankets, + number_of_blanket_segments, + blanket_fillet_radius=10.0, + **kwargs + ): + + self.gap_between_blankets = gap_between_blankets + self.number_of_blanket_segments = number_of_blanket_segments + self.blanket_fillet_radius = blanket_fillet_radius + + super().__init__(**kwargs) + + @property + def gap_between_blankets(self): + return self._gap_between_blankets + + @gap_between_blankets.setter + def gap_between_blankets(self, value): + """Sets the SegmentedBlanketBallReactor.gap_between_blankets + attribute which controls the horitzonal distance between blanket + segments.""" + if isinstance(value, (float, int)) and value > 0: + self._gap_between_blankets = float(value) + else: + raise ValueError( + "gap_between_blankets but be a positive value float") + + @property + def number_of_blanket_segments(self): + """Sets the SegmentedBlanketBallReactor.number_of_blanket_segments + attribute which controls the number of blanket segments.""" + return self._number_of_blanket_segments + + @number_of_blanket_segments.setter + def number_of_blanket_segments(self, value): + if isinstance(value, int) and value > 2: + self._number_of_blanket_segments = value + else: + raise ValueError( + "number_of_blanket_segments but be an int greater than 2") + + def _make_blankets_layers(self): + super()._make_blankets_layers() + azimuth_placement_angles = np.linspace( + 0, 360, self.number_of_blanket_segments, endpoint=False) + thin_cutter = paramak.BlanketCutterStar( + distance=self.gap_between_blankets, + azimuth_placement_angle=azimuth_placement_angles) + + thick_cutter = paramak.BlanketCutterStar( + distance=self.gap_between_blankets + + 2 * self.firstwall_radial_thickness, + azimuth_placement_angle=azimuth_placement_angles) + + self._blanket.cut = [self._center_column_cutter, thick_cutter] + + if self.blanket_fillet_radius != 0: + # tried firstwall start radius here already + x = self.major_radius + 1 + front_face_b = self._blanket.solid.faces( + cq.NearestToPointSelector((0, x, 0))) + front_edge_b = front_face_b.edges( + cq.NearestToPointSelector((0, x, 0))) + front_edge_length_b = front_edge_b.val().Length() + self._blanket.solid = self._blanket.solid.edges( + paramak.EdgeLengthSelector(front_edge_length_b)).fillet( + self.blanket_fillet_radius) + self._firstwall.thickness += self.blanket_radial_thickness + self._firstwall.cut = [ + self._center_column_cutter, + thin_cutter, + self._blanket] + + # TODO this segfaults at the moment but works as an opperation on the + # reactor after construction in jupyter + # tried different x values and (0, x, 0) + # noticed that it much quicker as a post process so perhaps some + # unwanted looping is happening + # if self.blanket_fillet_radius != 0: + # x = self.major_radius # tried firstwall start radius here already + # front_face = \ + # self._firstwall.solid.faces( + # cq.NearestToPointSelector((x, 0, 0))) + # print('found front face') + # front_edge = front_face.edges( + # cq.NearestToPointSelector((x, 0, 0))) + # print('found front edge') + # front_edge_length = front_edge.val().Length() + # print('found front edge length', front_edge_length) + # self._firstwall.solid = self._firstwall.solid.edges( + # paramak.EdgeLengthSelector(front_edge_length)).fillet(self.blanket_fillet_radius) + # print('finished') + + return [self._firstwall, self._blanket, self._blanket_rear_wall] diff --git a/paramak/parametric_reactors/single_null_ball_reactor.py b/paramak/parametric_reactors/single_null_ball_reactor.py new file mode 100644 index 000000000..cacb3a5b8 --- /dev/null +++ b/paramak/parametric_reactors/single_null_ball_reactor.py @@ -0,0 +1,21 @@ + +import paramak + + +class SingleNullBallReactor(paramak.BallReactor): + """Creates geometry for a single ball reactor with a single divertor + including a plasma, cylindrical center column shielding, square toroidal + field coils. There is no inboard breeder blanket on this ball reactor like + most spherical reactors. + + Arguments: + divertor_position (str): Defaults to "upper". + """ + + def __init__( + self, + divertor_position="upper", + **kwargs + ): + + super().__init__(divertor_position=divertor_position, **kwargs) diff --git a/paramak/parametric_reactors/single_null_submersion_reactor.py b/paramak/parametric_reactors/single_null_submersion_reactor.py new file mode 100644 index 000000000..590adfb6c --- /dev/null +++ b/paramak/parametric_reactors/single_null_submersion_reactor.py @@ -0,0 +1,26 @@ + +import paramak + + +class SingleNullSubmersionTokamak(paramak.SubmersionTokamak): + """Creates geometry for a submersion reactor with a single divertor + including a plasma, cylindrical center column shielding, square toroidal + field coils. There is an inboard breeder blanket on this submersion + reactor. + + Arguments: + divertor_position (str): Defaults to "upper". + support_position (str): Defaults to "upper". + """ + + def __init__( + self, + divertor_position="upper", + support_position="upper", + **kwargs + ): + + super().__init__( + divertor_position=divertor_position, + support_position=support_position, + **kwargs) diff --git a/paramak/parametric_reactors/submersion_reactor.py b/paramak/parametric_reactors/submersion_reactor.py new file mode 100644 index 000000000..4496f05fb --- /dev/null +++ b/paramak/parametric_reactors/submersion_reactor.py @@ -0,0 +1,699 @@ + +import warnings + +import cadquery as cq +import paramak + + +class SubmersionTokamak(paramak.Reactor): + """Creates geometry for a simple submersion reactor including a plasma, + cylindrical center column shielding, inboard and outboard breeder blanket, + divertor (upper and lower), support legs. Optional coat hanger shaped + toroidal field coils and pf coils. + + Arguments: + inner_bore_radial_thickness (float): the radial thickness of the + inner bore (cm) + inboard_tf_leg_radial_thickness (float): the radial thickness of + the inner leg of the toroidal field coils (cm) + center_column_shield_radial_thickness (float): the radial thickness + of the center column shield (cm) + inboard_blanket_radial_thickness (float): the radial thickness of + the inboard blanket (cm) + firstwall_radial_thickness (float): the radial thickness of the + first wall (cm) + inner_plasma_gap_radial_thickness (float): the radial thickness of + the inboard gap between the plasma and the center column shield + (cm) + plasma_radial_thickness (float): the radial thickness of the plasma + (cm) + divertor_radial_thickness (float): the radial thickness of the + divertors (cm) + support_radial_thickness (float): the radial thickness of the upper + and lower supports (cm) + outer_plasma_gap_radial_thickness (float): the radial thickness of + the outboard gap between the plasma and the first wall (cm) + outboard_blanket_radial_thickness (float): the radial thickness of + the blanket (cm) + blanket_rear_wall_radial_thickness (float): the radial thickness of + the rear wall of the blanket (cm) + elongation (float): the elongation of the plasma + triangularity (float): the triangularity of the plasma + number_of_tf_coils (int, optional): the number of tf coils. Defaults + to 16. + rotation_angle (float, optional): the angle of the sector that is + desired. Defaults to 360.0. + outboard_tf_coil_radial_thickness (float, optional): the radial + thickness of the toroidal field coil. Defaults to None. + tf_coil_to_rear_blanket_radial_gap (float, optional): the radial + distance between the rear of the blanket and the toroidal field + coil. Defaults to None. + outboard_tf_coil_poloidal_thickness (float, optional): the vertical + thickness of each poloidal field coil. Defaults to None. + pf_coil_vertical_thicknesses (list of floats, optional): the vertical + thickness of each poloidal field coil. Defaults to None. + pf_coil_radial_thicknesses (list of floats, optional): the radial + thickness of each poloidal field coil. Defaults to None. + pf_coil_to_tf_coil_radial_gap (float, optional): the radial distance + between the rear of the poloidal field coil and the toroidal field + coil. Defaults to None. + divertor_position (str, optional): the position of the divertor, + "upper", "lower" or "both". Defaults to "both". + support_position (str, optional): the position of the supports, + "upper", "lower" or "both". Defaults to "both". + """ + + def __init__( + self, + inner_bore_radial_thickness, + inboard_tf_leg_radial_thickness, + center_column_shield_radial_thickness, + inboard_blanket_radial_thickness, + firstwall_radial_thickness, + inner_plasma_gap_radial_thickness, + plasma_radial_thickness, + divertor_radial_thickness, + support_radial_thickness, + outer_plasma_gap_radial_thickness, + outboard_blanket_radial_thickness, + blanket_rear_wall_radial_thickness, + elongation, + triangularity, + number_of_tf_coils=16, + rotation_angle=360.0, + outboard_tf_coil_radial_thickness=None, + tf_coil_to_rear_blanket_radial_gap=None, + outboard_tf_coil_poloidal_thickness=None, + pf_coil_vertical_thicknesses=None, + pf_coil_radial_thicknesses=None, + pf_coil_to_tf_coil_radial_gap=None, + pf_coil_case_thickness=10, + divertor_position="both", + support_position="both", + ): + + super().__init__([]) + + self.inner_bore_radial_thickness = inner_bore_radial_thickness + self.inboard_tf_leg_radial_thickness = inboard_tf_leg_radial_thickness + self.center_column_shield_radial_thickness = ( + center_column_shield_radial_thickness + ) + self.inboard_blanket_radial_thickness = \ + inboard_blanket_radial_thickness + self.firstwall_radial_thickness = firstwall_radial_thickness + self.inner_plasma_gap_radial_thickness = \ + inner_plasma_gap_radial_thickness + self.plasma_radial_thickness = plasma_radial_thickness + self.outer_plasma_gap_radial_thickness = \ + outer_plasma_gap_radial_thickness + self.outboard_blanket_radial_thickness = \ + outboard_blanket_radial_thickness + self.blanket_rear_wall_radial_thickness = \ + blanket_rear_wall_radial_thickness + self.pf_coil_radial_thicknesses = pf_coil_radial_thicknesses + self.pf_coil_to_tf_coil_radial_gap = pf_coil_to_tf_coil_radial_gap + self.outboard_tf_coil_radial_thickness = \ + outboard_tf_coil_radial_thickness + self.outboard_tf_coil_poloidal_thickness = \ + outboard_tf_coil_poloidal_thickness + self.divertor_radial_thickness = divertor_radial_thickness + self.support_radial_thickness = support_radial_thickness + self.elongation = elongation + self.triangularity = triangularity + self.tf_coil_to_rear_blanket_radial_gap = \ + tf_coil_to_rear_blanket_radial_gap + self.pf_coil_vertical_thicknesses = pf_coil_vertical_thicknesses + self.pf_coil_case_thickness = pf_coil_case_thickness + self.number_of_tf_coils = number_of_tf_coils + self.rotation_angle = rotation_angle + self.divertor_position = divertor_position + self.support_position = support_position + # sets major radius and minor radius from equatorial_points to allow a + # radial build this helps avoid the plasma overlapping the center + # column and other components + + inner_equatorial_point = ( + inner_bore_radial_thickness + + inboard_tf_leg_radial_thickness + + center_column_shield_radial_thickness + + inboard_blanket_radial_thickness + + firstwall_radial_thickness + + inner_plasma_gap_radial_thickness + ) + outer_equatorial_point = \ + inner_equatorial_point + plasma_radial_thickness + self.major_radius = \ + (outer_equatorial_point + inner_equatorial_point) / 2 + self.minor_radius = self.major_radius - inner_equatorial_point + + @property + def pf_coil_radial_thicknesses(self): + return self._pf_coil_radial_thicknesses + + @pf_coil_radial_thicknesses.setter + def pf_coil_radial_thicknesses(self, value): + if not isinstance(value, list) and value is not None: + raise ValueError("pf_coil_radial_thicknesses must be a list") + self._pf_coil_radial_thicknesses = value + + @property + def pf_coil_vertical_thicknesses(self): + return self._pf_coil_vertical_thicknesses + + @pf_coil_vertical_thicknesses.setter + def pf_coil_vertical_thicknesses(self, value): + if not isinstance(value, list) and value is not None: + raise ValueError("pf_coil_vertical_thicknesses must be a list") + self._pf_coil_vertical_thicknesses = value + + @property + def divertor_position(self): + return self._divertor_position + + @divertor_position.setter + def divertor_position(self, value): + acceptable_values = ["upper", "lower", "both"] + if value in acceptable_values: + self._divertor_position = value + else: + msg = "divertor_position must be 'upper', 'lower' or 'both'" + raise ValueError(msg) + + @property + def support_position(self): + return self._support_position + + @support_position.setter + def support_position(self, value): + acceptable_values = ["upper", "lower", "both"] + if value in acceptable_values: + self._support_position = value + else: + msg = "support_position must be 'upper', 'lower' or 'both'" + raise ValueError(msg) + + def create_solids(self): + """Creates a list of paramak.Shape for components and saves it in + self.shapes_and_components + """ + + shapes_and_components = [] + + self._rotation_angle_check() + shapes_and_components.append(self._make_plasma()) + self._make_radial_build() + self._make_vertical_build() + shapes_and_components.append(self._make_center_column_shield()) + shapes_and_components.append(self._make_firstwall()) + shapes_and_components.append(self._make_blanket()) + shapes_and_components.append(self._make_divertor()) + shapes_and_components.append(self._make_supports()) + shapes_and_components.append(self._make_rear_blanket_wall()) + shapes_and_components += self._make_coils() + + self.shapes_and_components = shapes_and_components + + def _rotation_angle_check(self): + + if self.rotation_angle == 360: + msg = "360 degree rotation may result" + \ + " in a Standard_ConstructionError or AttributeError" + warnings.warn(msg, UserWarning) + + def _make_radial_build(self): + + # this is the radial build sequence, where one component stops and + # another starts + + self._inner_bore_start_radius = 0 + self._inner_bore_end_radius = ( + self._inner_bore_start_radius + self.inner_bore_radial_thickness + ) + + self._inboard_tf_coils_start_radius = self._inner_bore_end_radius + self._inboard_tf_coils_end_radius = ( + self._inboard_tf_coils_start_radius + + self.inboard_tf_leg_radial_thickness) + + self._center_column_shield_start_radius = \ + self._inboard_tf_coils_end_radius + self._center_column_shield_end_radius = ( + self._center_column_shield_start_radius + + self.center_column_shield_radial_thickness + ) + + self._inboard_blanket_start_radius = \ + self._center_column_shield_end_radius + self._inboard_blanket_end_radius = ( + self._inboard_blanket_start_radius + + self.inboard_blanket_radial_thickness) + + self._inboard_firstwall_start_radius = self._inboard_blanket_end_radius + self._inboard_firstwall_end_radius = ( + self._inboard_firstwall_start_radius + + self.firstwall_radial_thickness) + + self._inner_plasma_gap_start_radius = \ + self._inboard_firstwall_end_radius + self._inner_plasma_gap_end_radius = ( + self._inner_plasma_gap_start_radius + + self.inner_plasma_gap_radial_thickness) + + self._plasma_start_radius = self._inner_plasma_gap_end_radius + self._plasma_end_radius = \ + self._plasma_start_radius + \ + self.plasma_radial_thickness + + self._outer_plasma_gap_start_radius = self._plasma_end_radius + self._outer_plasma_gap_end_radius = ( + self._outer_plasma_gap_start_radius + + self.outer_plasma_gap_radial_thickness) + + self._outboard_firstwall_start_radius = \ + self._outer_plasma_gap_end_radius + self._outboard_firstwall_end_radius = ( + self._outboard_firstwall_start_radius + + self.firstwall_radial_thickness) + + self._outboard_blanket_start_radius = \ + self._outboard_firstwall_end_radius + self._outboard_blanket_end_radius = ( + self._outboard_blanket_start_radius + + self.outboard_blanket_radial_thickness) + + self._blanket_rear_wall_start_radius = \ + self._outboard_blanket_end_radius + self._blanket_rear_wall_end_radius = ( + self._blanket_rear_wall_start_radius + + self.blanket_rear_wall_radial_thickness) + + self._tf_info_provided = False + if ( + self.outboard_tf_coil_radial_thickness is not None + and self.tf_coil_to_rear_blanket_radial_gap is not None + and self.outboard_tf_coil_poloidal_thickness is not None + ): + self._tf_info_provided = True + self._outboard_tf_coil_start_radius = ( + self._blanket_rear_wall_end_radius + + self.tf_coil_to_rear_blanket_radial_gap) + self._outboard_tf_coil_end_radius = ( + self._outboard_tf_coil_start_radius + + self.outboard_tf_coil_radial_thickness) + + self._pf_info_provided = False + if ( + self.pf_coil_vertical_thicknesses is not None + and self.pf_coil_radial_thicknesses is not None + and self.pf_coil_to_tf_coil_radial_gap is not None + ): + self._pf_info_provided = True + + self._divertor_start_radius = ( + self._plasma.high_point[0] - 0.5 * self.divertor_radial_thickness + ) + self._divertor_end_radius = ( + self._plasma.high_point[0] + 0.5 * self.divertor_radial_thickness + ) + + self._support_start_radius = ( + self._plasma.high_point[0] - 0.5 * self.support_radial_thickness + ) + self._support_end_radius = ( + self._plasma.high_point[0] + 0.5 * self.support_radial_thickness + ) + + def _make_vertical_build(self): + + # this is the vertical build sequence, componets build on each other in + # a similar manner to the radial build + + self._plasma_start_height = 0 + self._plasma_end_height = self._plasma.high_point[1] + + self._plasma_to_divertor_gap_start_height = self._plasma_end_height + self._plasma_to_divertor_gap_end_height = ( + self._plasma_to_divertor_gap_start_height + + self.outer_plasma_gap_radial_thickness) + + # the firstwall is cut by the divertor but uses the same control points + self._firstwall_start_height = self._plasma_to_divertor_gap_end_height + self._firstwall_end_height = self._firstwall_start_height + \ + self.firstwall_radial_thickness + + self._blanket_start_height = self._firstwall_end_height + self._blanket_end_height = ( + self._blanket_start_height + self.outboard_blanket_radial_thickness + ) + + self._blanket_rear_wall_start_height = self._blanket_end_height + self._blanket_rear_wall_end_height = ( + self._blanket_rear_wall_start_height + + self.blanket_rear_wall_radial_thickness) + + if self._tf_info_provided: + self._outboard_tf_coils_vertical_height = \ + self._blanket_rear_wall_end_height * 1.5 + self._outboard_tf_coils_horizontal_length = \ + self._blanket_rear_wall_end_radius * 0.75 + + if self._tf_info_provided and self._pf_info_provided: + self._number_of_pf_coils = len(self.pf_coil_vertical_thicknesses) + + y_position_step = (2 * self._blanket_rear_wall_end_height) / ( + self._number_of_pf_coils + 1 + ) + + if not isinstance(self.pf_coil_case_thickness, list): + self.pf_coil_case_thickness = [ + self.pf_coil_case_thickness] * self._number_of_pf_coils + + self._pf_coils_xy_values = [] + # adds in coils with equal spacing strategy, should be updated to + # allow user positions + for i in range(self._number_of_pf_coils): + y_value = ( + self._blanket_rear_wall_end_height + + self.pf_coil_to_tf_coil_radial_gap + - y_position_step * (i + 1) + ) + x_value = ( + self._outboard_tf_coil_end_radius + + self.pf_coil_to_tf_coil_radial_gap + + 0.5 * self.pf_coil_radial_thicknesses[i] + + self.pf_coil_case_thickness[i] + ) + self._pf_coils_xy_values.append((x_value, y_value)) + + self._pf_coil_start_radius = ( + self._outboard_tf_coil_end_radius + + self.pf_coil_to_tf_coil_radial_gap) + self._pf_coil_end_radius = self._pf_coil_start_radius + max( + self.pf_coil_radial_thicknesses + ) + + def _make_center_column_shield(self): + + self._center_column_shield = paramak.CenterColumnShieldCylinder( + height=self._blanket_rear_wall_end_height * 2, + inner_radius=self._center_column_shield_start_radius, + outer_radius=self._center_column_shield_end_radius, + rotation_angle=self.rotation_angle, + stp_filename="center_column_shield.stp", + stl_filename="center_column_shield.stl", + name="center_column_shield", + material_tag="center_column_shield_mat", + ) + return self._center_column_shield + + def _make_plasma(self): + + plasma = paramak.Plasma( + major_radius=self.major_radius, + minor_radius=self.minor_radius, + elongation=self.elongation, + triangularity=self.triangularity, + rotation_angle=self.rotation_angle, + ) + + self._plasma = plasma + return self._plasma + + def _make_firstwall(self): + + # this is used to cut the inboard blanket and then fused / unioned with + # the firstwall + self._inboard_firstwall = paramak.BlanketFP( + plasma=self._plasma, + offset_from_plasma=self.inner_plasma_gap_radial_thickness, + start_angle=90, + stop_angle=270, + thickness=self.firstwall_radial_thickness, + rotation_angle=self.rotation_angle, + ) + + self._firstwall = paramak.BlanketFP( + plasma=self._plasma, + offset_from_plasma=self.outer_plasma_gap_radial_thickness, + start_angle=90, + stop_angle=-90, + thickness=self.firstwall_radial_thickness, + rotation_angle=self.rotation_angle, + stp_filename="outboard_firstwall.stp", + stl_filename="outboard_firstwall.stl", + name="outboard_firstwall", + material_tag="firstwall_mat", + union=self._inboard_firstwall, + ) + return self._firstwall + + def _make_divertor(self): + fw_enveloppe_inboard = paramak.BlanketFP( + plasma=self._plasma, + offset_from_plasma=self.inner_plasma_gap_radial_thickness, + start_angle=90, + stop_angle=270, + thickness=self.firstwall_radial_thickness, + rotation_angle=self.rotation_angle, + ) + + fw_enveloppe = paramak.BlanketFP( + plasma=self._plasma, + offset_from_plasma=self.outer_plasma_gap_radial_thickness, + start_angle=90, + stop_angle=-90, + thickness=self.firstwall_radial_thickness, + rotation_angle=self.rotation_angle, + stp_filename="outboard_firstwall.stp", + stl_filename="outboard_firstwall.stl", + name="outboard_firstwall", + material_tag="firstwall_mat", + union=fw_enveloppe_inboard, + ) + divertor_height = self._blanket_rear_wall_end_height + + divertor_height_top = divertor_height + divertor_height_bottom = -divertor_height + + if self.divertor_position == "lower": + divertor_height_top = 0 + elif self.divertor_position == "upper": + divertor_height_bottom = 0 + + self._divertor = paramak.RotateStraightShape( + points=[ + (self._divertor_start_radius, divertor_height_bottom), + (self._divertor_end_radius, divertor_height_bottom), + (self._divertor_end_radius, divertor_height_top), + (self._divertor_start_radius, divertor_height_top) + ], + intersect=fw_enveloppe, + rotation_angle=self.rotation_angle, + stp_filename="divertor.stp", + stl_filename="divertor.stl", + name="divertor", + material_tag="divertor_mat" + ) + + self._firstwall.cut = self._divertor + self._inboard_firstwall.cut = self._divertor + return self._divertor + + def _make_blanket(self): + self._inboard_blanket = paramak.CenterColumnShieldCylinder( + height=self._blanket_end_height * 2, + inner_radius=self._inboard_blanket_start_radius, + outer_radius=max(self._inboard_firstwall.points)[0], + rotation_angle=self.rotation_angle, + cut=self._inboard_firstwall, + ) + + # this takes a single solid from a compound of solids by finding the + # solid nearest to a point + # TODO: find alternative + self._inboard_blanket.solid = self._inboard_blanket.solid.solids( + cq.selectors.NearestToPointSelector((0, 0, 0)) + ) + # this is the outboard fused /unioned with the inboard blanket + + self._blanket = paramak.BlanketFP( + plasma=self._plasma, + start_angle=90, + stop_angle=-90, + offset_from_plasma=self.outer_plasma_gap_radial_thickness + + self.firstwall_radial_thickness, + thickness=self.outboard_blanket_radial_thickness, + rotation_angle=self.rotation_angle, + stp_filename="blanket.stp", + stl_filename="blanket.stl", + name="blanket", + material_tag="blanket_mat", + union=self._inboard_blanket, + ) + return self._blanket + + def _make_supports(self): + blanket_enveloppe = paramak.BlanketFP( + plasma=self._plasma, + start_angle=90, + stop_angle=-90, + offset_from_plasma=self.outer_plasma_gap_radial_thickness + + self.firstwall_radial_thickness, + thickness=self.outboard_blanket_radial_thickness, + rotation_angle=self.rotation_angle, + union=self._inboard_blanket, + ) + support_height = self._blanket_rear_wall_end_height + support_height_top = support_height + support_height_bottom = -support_height + + if self.support_position == "lower": + support_height_top = 0 + elif self.support_position == "upper": + support_height_bottom = 0 + + self._supports = paramak.RotateStraightShape( + points=[ + (self._support_start_radius, support_height_bottom), + (self._support_end_radius, support_height_bottom), + (self._support_end_radius, support_height_top), + (self._support_start_radius, support_height_top) + ], + rotation_angle=self.rotation_angle, + stp_filename="supports.stp", + stl_filename="supports.stl", + name="supports", + material_tag="supports_mat", + intersect=blanket_enveloppe, + ) + self._blanket.cut = self._supports + + return self._supports + + def _make_rear_blanket_wall(self): + self._outboard_rear_blanket_wall_upper = paramak.RotateStraightShape( + points=[ + ( + self._center_column_shield_end_radius, + self._blanket_rear_wall_start_height + ), + ( + self._center_column_shield_end_radius, + self._blanket_rear_wall_end_height + ), + ( + max(self._inboard_firstwall.points)[0], + self._blanket_rear_wall_end_height, + ), + ( + max(self._inboard_firstwall.points)[0], + self._blanket_rear_wall_start_height, + ), + ], + rotation_angle=self.rotation_angle, + ) + + self._outboard_rear_blanket_wall_lower = paramak.RotateStraightShape( + points=[ + ( + self._center_column_shield_end_radius, + -self._blanket_rear_wall_start_height + ), + ( + self._center_column_shield_end_radius, + -self._blanket_rear_wall_end_height + ), + ( + max(self._inboard_firstwall.points)[0], + -self._blanket_rear_wall_end_height, + ), + ( + max(self._inboard_firstwall.points)[0], + -self._blanket_rear_wall_start_height, + ), + ], + rotation_angle=self.rotation_angle, + ) + + self._outboard_rear_blanket_wall = paramak.BlanketFP( + plasma=self._plasma, + start_angle=90, + stop_angle=-90, + offset_from_plasma=self.outer_plasma_gap_radial_thickness + + self.firstwall_radial_thickness + + self.outboard_blanket_radial_thickness, + thickness=self.blanket_rear_wall_radial_thickness, + rotation_angle=self.rotation_angle, + stp_filename="outboard_rear_blanket_wall.stp", + stl_filename="outboard_rear_blanket_wall.stl", + name="outboard_rear_blanket_wall", + material_tag="blanket_rear_wall_mat", + union=[ + self._outboard_rear_blanket_wall_upper, + self._outboard_rear_blanket_wall_lower], + ) + + return self._outboard_rear_blanket_wall + + def _make_coils(self): + list_of_components = [] + + self._inboard_tf_coils = paramak.CenterColumnShieldCylinder( + height=self._blanket_rear_wall_end_height * 2, + inner_radius=self._inboard_tf_coils_start_radius, + outer_radius=self._inboard_tf_coils_end_radius, + rotation_angle=self.rotation_angle, + stp_filename="inboard_tf_coils.stp", + stl_filename="inboard_tf_coils.stl", + name="inboard_tf_coils", + material_tag="inboard_tf_coils_mat", + ) + list_of_components.append(self._inboard_tf_coils) + if self._tf_info_provided: + self._tf_coil = paramak.ToroidalFieldCoilCoatHanger( + with_inner_leg=False, + horizontal_start_point=( + self._inboard_tf_coils_start_radius, + self._blanket_rear_wall_end_height, + ), + vertical_mid_point=(self._outboard_tf_coil_start_radius, 0), + thickness=self.outboard_tf_coil_radial_thickness, + number_of_coils=self.number_of_tf_coils, + distance=self.outboard_tf_coil_poloidal_thickness, + stp_filename="outboard_tf_coil.stp", + stl_filename="outboard_tf_coil.stl", + rotation_angle=self.rotation_angle, + horizontal_length=self._outboard_tf_coils_horizontal_length, + vertical_length=self._outboard_tf_coils_vertical_height + ) + list_of_components.append(self._tf_coil) + + if self._pf_info_provided: + + self._pf_coil = paramak.PoloidalFieldCoilSet( + heights=self.pf_coil_vertical_thicknesses, + widths=self.pf_coil_radial_thicknesses, + center_points=self._pf_coils_xy_values, + rotation_angle=self.rotation_angle, + stp_filename='pf_coils.stp', + stl_filename='pf_coils.stl', + name="pf_coil", + material_tag="pf_coil_mat", + ) + + list_of_components.append(self._pf_coil) + + self._pf_coils_casing = paramak.PoloidalFieldCoilCaseSetFC( + pf_coils=self._pf_coil, + casing_thicknesses=self.pf_coil_case_thickness, + rotation_angle=self.rotation_angle, + stp_filename='pf_coil_cases.stp', + stl_filename='pf_coil_cases.stl', + name="pf_coil_case", + material_tag="pf_coil_case_mat", + ) + + list_of_components.append(self._pf_coils_casing) + + return list_of_components diff --git a/paramak/parametric_shapes/__init__.py b/paramak/parametric_shapes/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/paramak/parametric_shapes/extruded_circle_shape.py b/paramak/parametric_shapes/extruded_circle_shape.py new file mode 100644 index 000000000..d537910d5 --- /dev/null +++ b/paramak/parametric_shapes/extruded_circle_shape.py @@ -0,0 +1,108 @@ + +import cadquery as cq +from paramak import Shape +from paramak.utils import calculate_wedge_cut + + +class ExtrudeCircleShape(Shape): + """Extrudes a circular 3d CadQuery solid from a central point and a radius + + Args: + distance (float): the extrusion distance to use (cm units if used for + neutronics) + radius (float): radius of the shape. + rotation_angle (float): rotation_angle of solid created. a cut is + performed from rotation_angle to 360 degrees. Defaults to 360. + extrude_both (bool, optional): if set to True, the extrusion will + occur in both directions. Defaults to True. + stp_filename (str, optional): Defaults to "ExtrudeCircleShape.stp". + stl_filename (str, optional): Defaults to "ExtrudeCircleShape.stl". + """ + + def __init__( + self, + distance, + radius, + extrusion_start_offset=0.0, + rotation_angle=360, + extrude_both=True, + stp_filename="ExtrudeCircleShape.stp", + stl_filename="ExtrudeCircleShape.stl", + **kwargs + ): + + super().__init__( + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + self.radius = radius + self.distance = distance + self.rotation_angle = rotation_angle + self.extrude_both = extrude_both + self.extrusion_start_offset = extrusion_start_offset + + @property + def radius(self): + return self._radius + + @radius.setter + def radius(self, value): + self._radius = value + + @property + def distance(self): + return self._distance + + @distance.setter + def distance(self, value): + self._distance = value + + @property + def rotation_angle(self): + return self._rotation_angle + + @rotation_angle.setter + def rotation_angle(self, value): + self._rotation_angle = value + + @property + def extrusion_start_offset(self): + return self._extrusion_start_offset + + @extrusion_start_offset.setter + def extrusion_start_offset(self, value): + self._extrusion_start_offset = value + + def create_solid(self): + """Creates an extruded 3d solid using points connected with circular + edges. + + :return: a 3d solid volume + :rtype: a cadquery solid + """ + + # so a positive offset moves extrusion further from axis of azimuthal + # placement rotation + extrusion_offset = -self.extrusion_start_offset + + if not self.extrude_both: + extrusion_distance = -self.distance + else: + extrusion_distance = -self.distance / 2.0 + + solid = ( + cq.Workplane(self.workplane) + .workplane(offset=extrusion_offset) + .moveTo(self.points[0][0], self.points[0][1]) + .circle(self.radius) + .extrude(distance=extrusion_distance, both=self.extrude_both) + ) + + solid = self.rotate_solid(solid) + cutting_wedge = calculate_wedge_cut(self) + solid = self.perform_boolean_operations(solid, wedge_cut=cutting_wedge) + self.solid = solid + + return solid diff --git a/paramak/parametric_shapes/extruded_mixed_shape.py b/paramak/parametric_shapes/extruded_mixed_shape.py new file mode 100644 index 000000000..fa1cb9a7d --- /dev/null +++ b/paramak/parametric_shapes/extruded_mixed_shape.py @@ -0,0 +1,92 @@ + +from paramak import Shape +from paramak.utils import calculate_wedge_cut + + +class ExtrudeMixedShape(Shape): + """Extrudes a 3d CadQuery solid from points connected with a mixture of + straight and spline connections. + + Args: + distance (float): the extrusion distance to use (cm units if used for + neutronics) + extrude_both (bool, optional): If set to True, the extrusion will + occur in both directions. Defaults to True. + rotation_angle (float, optional): rotation angle of solid created. A + cut is performed from rotation_angle to 360 degrees. Defaults to + 360.0. + stp_filename (str, optional): Defaults to "ExtrudeMixedShape.stp". + stl_filename (str, optional): Defaults to "ExtrudeMixedShape.stl". + + """ + + def __init__( + self, + distance, + extrude_both=True, + rotation_angle=360.0, + extrusion_start_offset=0.0, + stp_filename="ExtrudeMixedShape.stp", + stl_filename="ExtrudeMixedShape.stl", + **kwargs + ): + + super().__init__( + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + self.distance = distance + self.extrude_both = extrude_both + self.rotation_angle = rotation_angle + self.extrusion_start_offset = extrusion_start_offset + + @property + def distance(self): + return self._distance + + @distance.setter + def distance(self, value): + self._distance = value + + @property + def rotation_angle(self): + return self._rotation_angle + + @rotation_angle.setter + def rotation_angle(self, value): + self._rotation_angle = value + + @property + def extrusion_start_offset(self): + return self._extrusion_start_offset + + @extrusion_start_offset.setter + def extrusion_start_offset(self, value): + self._extrusion_start_offset = value + + def create_solid(self): + """Creates an extruded 3d solid using points connected with straight + and spline edges. + + Returns: + A CadQuery solid: A 3D solid volume + """ + + solid = super().create_solid() + + if not self.extrude_both: + extrusion_distance = -self.distance + else: + extrusion_distance = -self.distance / 2.0 + + solid = solid.close().extrude( + distance=extrusion_distance, + both=self.extrude_both) + + solid = self.rotate_solid(solid) + cutting_wedge = calculate_wedge_cut(self) + solid = self.perform_boolean_operations(solid, wedge_cut=cutting_wedge) + self.solid = solid + + return solid diff --git a/paramak/parametric_shapes/extruded_spline_shape.py b/paramak/parametric_shapes/extruded_spline_shape.py new file mode 100644 index 000000000..016640c6e --- /dev/null +++ b/paramak/parametric_shapes/extruded_spline_shape.py @@ -0,0 +1,30 @@ + +from paramak import ExtrudeMixedShape + + +class ExtrudeSplineShape(ExtrudeMixedShape): + """Extrudes a 3d CadQuery solid from points connected with spline + connections. + + Args: + distance (float): the extrusion distance to use (cm units if used for + neutronics). + stp_filename (str, optional): Defaults to "ExtrudeSplineShape.stp". + stl_filename (str, optional): Defaults to "ExtrudeSplineShape.stl". + """ + + def __init__( + self, + distance, + stp_filename="ExtrudeSplineShape.stp", + stl_filename="ExtrudeSplineShape.stl", + **kwargs + ): + + super().__init__( + distance=distance, + stp_filename=stp_filename, + stl_filename=stl_filename, + connection_type="spline", + **kwargs + ) diff --git a/paramak/parametric_shapes/extruded_straight_shape.py b/paramak/parametric_shapes/extruded_straight_shape.py new file mode 100644 index 000000000..423937541 --- /dev/null +++ b/paramak/parametric_shapes/extruded_straight_shape.py @@ -0,0 +1,29 @@ + +from paramak import ExtrudeMixedShape + + +class ExtrudeStraightShape(ExtrudeMixedShape): + """Extrudes a 3d CadQuery solid from points connected with straight lines. + + Args: + distance (float): the extrusion distance to use (cm units if used for + neutronics) + stp_filename (str, optional): Defaults to "ExtrudeStraightShape.stp". + stl_filename (str, optional): Defaults to "ExtrudeStraightShape.stl". + """ + + def __init__( + self, + distance, + stp_filename="ExtrudeStraightShape.stp", + stl_filename="ExtrudeStraightShape.stl", + **kwargs + ): + + super().__init__( + distance=distance, + stp_filename=stp_filename, + stl_filename=stl_filename, + connection_type="straight", + **kwargs + ) diff --git a/paramak/parametric_shapes/rotate_circle_shape.py b/paramak/parametric_shapes/rotate_circle_shape.py new file mode 100644 index 000000000..1efb00e62 --- /dev/null +++ b/paramak/parametric_shapes/rotate_circle_shape.py @@ -0,0 +1,68 @@ + +import cadquery as cq +from paramak import Shape + + +class RotateCircleShape(Shape): + """Rotates a circular 3d CadQuery solid from a central point and a radius + + Args: + radius (float): radius of the shape + rotation_angle (float, optional): The rotation_angle to use when + revolving the solid (degrees). Defaults to 360.0. + stp_filename (str, optional): Defaults to "RotateCircleShape.stp". + stl_filename (str, optional): Defaults to "RotateCircleShape.stl". + """ + + def __init__( + self, + radius, + rotation_angle=360.0, + stp_filename="RotateCircleShape.stp", + stl_filename="RotateCircleShape.stl", + **kwargs + ): + + super().__init__( + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + self.radius = radius + self.rotation_angle = rotation_angle + + @property + def rotation_angle(self): + return self._rotation_angle + + @rotation_angle.setter + def rotation_angle(self, value): + self._rotation_angle = value + + @property + def radius(self): + return self._radius + + @radius.setter + def radius(self, value): + self._radius = value + + def create_solid(self): + """Creates a rotated 3d solid using points with circular edges. + + Returns: + A CadQuery solid: A 3D solid volume + """ + + solid = ( + cq.Workplane(self.workplane) + .moveTo(self.points[0][0], self.points[0][1]) + .circle(self.radius) + # .close() + .revolve(self.rotation_angle) + ) + + solid = self.rotate_solid(solid) + solid = self.perform_boolean_operations(solid) + self.solid = solid + return solid diff --git a/paramak/parametric_shapes/rotate_mixed_shape.py b/paramak/parametric_shapes/rotate_mixed_shape.py new file mode 100644 index 000000000..bf488e9bf --- /dev/null +++ b/paramak/parametric_shapes/rotate_mixed_shape.py @@ -0,0 +1,54 @@ + +from paramak import Shape + + +class RotateMixedShape(Shape): + """Rotates a 3d CadQuery solid from points connected with a mixture of + straight lines and splines. + + Args: + rotation_angle (float, optional): The rotation_angle to use when + revolving the solid (degrees). Defaults to 360.0. + stp_filename (str, optional): Defaults to "RotateMixedShape.stp". + stl_filename (str, optional): Defaults to "RotateMixedShape.stl". + """ + + def __init__( + self, + rotation_angle=360.0, + stp_filename="RotateMixedShape.stp", + stl_filename="RotateMixedShape.stl", + **kwargs + ): + + super().__init__( + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + self.rotation_angle = rotation_angle + + @property + def rotation_angle(self): + return self._rotation_angle + + @rotation_angle.setter + def rotation_angle(self, value): + self._rotation_angle = value + + def create_solid(self): + """Creates a rotated 3d solid using points with straight and spline + edges. + + Returns: + A CadQuery solid: A 3D solid volume + """ + + solid = super().create_solid() + + solid = solid.close().revolve(self.rotation_angle) + + solid = self.rotate_solid(solid) + solid = self.perform_boolean_operations(solid) + self.solid = solid + return solid diff --git a/paramak/parametric_shapes/rotate_spline_shape.py b/paramak/parametric_shapes/rotate_spline_shape.py new file mode 100644 index 000000000..a5f71315c --- /dev/null +++ b/paramak/parametric_shapes/rotate_spline_shape.py @@ -0,0 +1,29 @@ + +from paramak import RotateMixedShape + + +class RotateSplineShape(RotateMixedShape): + """Rotates a 3d CadQuery solid from points connected with splines. + + Args: + rotation_angle (float, optional): The rotation_angle to use when + revolving the solid (degrees). Defaults to 360.0. + stp_filename (str, optional): Defaults to "RotateSplineShape.stp". + stl_filename (str, optional): Defaults to "RotateSplineShape.stl". + """ + + def __init__( + self, + rotation_angle=360, + stp_filename="RotateSplineShape.stp", + stl_filename="RotateSplineShape.stl", + **kwargs + ): + + super().__init__( + rotation_angle=rotation_angle, + stp_filename=stp_filename, + stl_filename=stl_filename, + connection_type="spline", + **kwargs + ) diff --git a/paramak/parametric_shapes/rotate_straight_shape.py b/paramak/parametric_shapes/rotate_straight_shape.py new file mode 100644 index 000000000..184b899fc --- /dev/null +++ b/paramak/parametric_shapes/rotate_straight_shape.py @@ -0,0 +1,30 @@ + +from paramak import RotateMixedShape + + +class RotateStraightShape(RotateMixedShape): + """Rotates a 3d CadQuery solid from points connected with straight + connections. + + Args: + rotation_angle (float): The rotation angle to use when revolving the + solid (degrees). + stp_filename (str, optional): Defaults to "RotateStraightShape.stp". + stl_filename (str, optional): Defaults to "RotateStraightShape.stl". + """ + + def __init__( + self, + rotation_angle=360.0, + stp_filename="RotateStraightShape.stp", + stl_filename="RotateStraightShape.stl", + **kwargs + ): + + super().__init__( + rotation_angle=rotation_angle, + stp_filename=stp_filename, + stl_filename=stl_filename, + connection_type="straight", + **kwargs + ) diff --git a/paramak/parametric_shapes/sweep_circle_shape.py b/paramak/parametric_shapes/sweep_circle_shape.py new file mode 100644 index 000000000..1a7179252 --- /dev/null +++ b/paramak/parametric_shapes/sweep_circle_shape.py @@ -0,0 +1,129 @@ + +import cadquery as cq +from paramak import Shape + + +class SweepCircleShape(Shape): + """Sweeps a 2D circle of a defined radius along a defined spline path to + create a 3D CadQuery solid. Note, some variation in the cross-section of + the solid may occur. + + Args: + radius (float): Radius of 2D circle to be swept. + path_points (list of tuples each containing X (float), Z (float)): A + list of XY, YZ or XZ coordinates connected by spline connections + which define the path along which the 2D shape is swept. + workplane (str, optional): Workplane in which the circle to be swept + is defined. Defaults to "XY". + path_workplane (str, optional): Workplane in which the spline path is + defined. Defaults to "XZ". + stp_filename (str, optional): Defaults to "SweepCircleShape.stp". + stl_filename (str, optional): Defaults to "SweepCircleShape.stl". + force_cross_section (bool, optional): If True, cross-section of solid + is forced to be shape defined by points in workplane at each + path_point. Defaults to False. + """ + + def __init__( + self, + radius, + path_points, + workplane="XY", + path_workplane="XZ", + stp_filename="SweepCircleShape.stp", + stl_filename="SweepCircleShape.stl", + force_cross_section=False, + **kwargs + ): + + super().__init__( + workplane=workplane, + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + self.radius = radius + self.path_points = path_points + self.path_workplane = path_workplane + self.force_cross_section = force_cross_section + + @property + def radius(self): + return self._radius + + @radius.setter + def radius(self, value): + self._radius = value + + @property + def path_points(self): + return self._path_points + + @path_points.setter + def path_points(self, value): + self._points = value + self._path_points = value + + @property + def path_workplane(self): + return self._path_workplane + + @path_workplane.setter + def path_workplane(self, value): + if value[0] != self.workplane[0]: + raise ValueError( + "workplane and path_workplane must start with the same letter" + ) + elif value == self.workplane: + raise ValueError( + "workplane and path_workplane must be different" + ) + else: + self._path_workplane = value + + def create_solid(self): + """Creates a swept 3D solid from a 2D circle. + + Returns: + A CadQuery solid: A 3D solid volume + """ + + path = cq.Workplane(self.path_workplane).spline(self.path_points) + + factor = 1 + if self.workplane in ["XZ", "YX", "ZY"]: + factor *= -1 + + if self.force_cross_section: + solid = cq.Workplane(self.workplane).moveTo(0, 0) + for point in self.path_points[:-1]: + solid = solid.workplane(offset=point[1] * factor).\ + moveTo(point[0], 0).\ + circle(self.radius).\ + moveTo(0, 0).\ + workplane(offset=-point[1] * factor) + solid = solid.workplane(offset=self.path_points[-1][1] * factor).moveTo( + self.path_points[-1][0], 0).circle(self.radius).sweep(path, multisection=True) + + if not self.force_cross_section: + + solid = ( + cq.Workplane(self.workplane) + .workplane(offset=self.path_points[0][1] * factor) + .moveTo(self.path_points[0][0], 0) + .workplane() + .circle(self.radius) + .moveTo(-self.path_points[0][0], 0) + .workplane(offset=-self.path_points[0][1] * factor) + .workplane(offset=self.path_points[-1][1] * factor) + .moveTo(self.path_points[-1][0], 0) + .workplane() + .circle(self.radius) + .sweep(path, multisection=True) + ) + + solid = self.rotate_solid(solid) + solid = self.perform_boolean_operations(solid) + self.solid = solid + return solid diff --git a/paramak/parametric_shapes/sweep_mixed_shape.py b/paramak/parametric_shapes/sweep_mixed_shape.py new file mode 100644 index 000000000..5b444a67e --- /dev/null +++ b/paramak/parametric_shapes/sweep_mixed_shape.py @@ -0,0 +1,87 @@ + +import cadquery as cq +from paramak import Shape + + +class SweepMixedShape(Shape): + """Sweeps a 2D shape created from points connected with straight, spline + or circle connections along a defined spline path to create a 3D CadQuery + solid. Note, some variation in cross-section of the solid may occur. + + Args: + path_points (list of tuples each containing X (float), Z (float)): A + list of XY, YZ or XZ coordinates connected by spline connections + which define the path along which the 2D shape is swept. + workplane (str, optional): Workplane in which the 2D shape to be swept + is defined. Defaults to "XY". + path_workplane (str, optional): Workplane in which the spline path is + defined. Defaults to "XZ". + stp_filename (str, optional): Defaults to "SweepMixedShape.stp". + stl_filename (str, optional): Defaults to "SweepMixedShape.stl". + force_cross_section (bool, optional): If True, cross-section of solid + is forced to be shape defined by points in workplane at each + path_point. Defaults to False. + """ + + def __init__( + self, + path_points, + workplane="XY", + path_workplane="XZ", + stp_filename="SweepMixedShape.stp", + stl_filename="SweepMixedShape.stl", + force_cross_section=False, + **kwargs + ): + + super().__init__( + workplane=workplane, + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + self.path_points = path_points + self.path_workplane = path_workplane + self.force_cross_section = force_cross_section + + @property + def path_points(self): + return self._path_points + + @path_points.setter + def path_points(self, value): + self._path_points = value + + @property + def path_workplane(self): + return self._path_workplane + + @path_workplane.setter + def path_workplane(self, value): + if value[0] != self.workplane[0]: + raise ValueError( + "workplane and path_workplane must start with the same letter" + ) + elif value == self.workplane: + raise ValueError( + "workplane and path_workplane must be different" + ) + else: + self._path_workplane = value + + def create_solid(self): + """Creates a swept 3D solid from a 2D shape with mixed connections. + + Returns: + A CadQuery solid: A 3D solid volume + """ + + solid = super().create_solid() + path = cq.Workplane(self.path_workplane).spline(self.path_points) + solid = solid.close().sweep(path, multisection=True) + + solid = self.rotate_solid(solid) + solid = self.perform_boolean_operations(solid) + self.solid = solid + return solid diff --git a/paramak/parametric_shapes/sweep_spline_shape.py b/paramak/parametric_shapes/sweep_spline_shape.py new file mode 100644 index 000000000..0605bf04b --- /dev/null +++ b/paramak/parametric_shapes/sweep_spline_shape.py @@ -0,0 +1,45 @@ + +from paramak import SweepMixedShape + + +class SweepSplineShape(SweepMixedShape): + """Sweeps a 2D shape created from points connected with spline connections + along a defined spline path to create a 3D CadQuery solid. Note, some + variation in the cross-section of the solid may occur. + + Args: + path_points (list of tuples each containing X (float), Z (float)): A + list of XY, YZ or XZ coordinates connected by spline connections + which define the path along which the 2D shape is swept. + workplane (str, optional): Workplane in which the 2D shape to be swept + is defined. Defaults to "XY". + path_workplane (str, optional): Workplane in which the spline path is + defined. Defaults to "XZ". + stp_filename (str, optional): Defaults to "SweepSplineShape.stp". + stl_filename (str, optional): Defaults to "SweepSplineShape.stl". + force_cross_section (bool, optional): If True, cross-setion of solid + is forced to be shape defined by points in workplane at each + path_point. Defaults to False. + """ + + def __init__( + self, + path_points, + workplane="XY", + path_workplane="XZ", + stp_filename="SweepSplineShape.stp", + stl_filename="SweepSplineShape.stl", + force_cross_section=False, + **kwargs + ): + + super().__init__( + path_points=path_points, + workplane=workplane, + path_workplane=path_workplane, + stp_filename=stp_filename, + stl_filename=stl_filename, + connection_type="spline", + force_cross_section=force_cross_section, + **kwargs + ) diff --git a/paramak/parametric_shapes/sweep_straight_shape.py b/paramak/parametric_shapes/sweep_straight_shape.py new file mode 100644 index 000000000..0464806d5 --- /dev/null +++ b/paramak/parametric_shapes/sweep_straight_shape.py @@ -0,0 +1,45 @@ + +from paramak import SweepMixedShape + + +class SweepStraightShape(SweepMixedShape): + """Sweeps a 2D shape created from points connected with straight lines + along a defined spline path to create a 3D CadQuery solid. Note, some + variation in the cross-section of the solid may occur. + + Args: + path_points (list of tuples each containing X (float), Z (float)): A + list of XY, YZ or XZ coordinates connected by spline connections + which define the path along which the 2D shape is swept. + workplane (str, optional): Workplane in which the 2D shape to be swept + is defined. Defaults to "XY". + path_workplane (str, optional): Workplane in which the spline path is + defined. Defaults to "XZ". + stp_filename (str, optional): Defaults to "SweepStraightShape.stp". + stl_filename (str, optional): Defaults to "SweepStraightShape.stl". + force_cross_section (bool, optional): If True, cross-section of solid + is forced to be shape defined by points in workplane at each + path_point. Defaults to False. + """ + + def __init__( + self, + path_points, + workplane="XY", + path_workplane="XZ", + stp_filename="SweepStraightShape.stp", + stl_filename="SweepStraightShape.stl", + force_cross_section=False, + **kwargs + ): + + super().__init__( + path_points=path_points, + workplane=workplane, + path_workplane=path_workplane, + stp_filename=stp_filename, + stl_filename=stl_filename, + connection_type="straight", + force_cross_section=force_cross_section, + **kwargs + ) diff --git a/paramak/reactor.py b/paramak/reactor.py new file mode 100644 index 000000000..9c18c5c62 --- /dev/null +++ b/paramak/reactor.py @@ -0,0 +1,615 @@ + +import json +from collections import Iterable +from pathlib import Path + +import cadquery as cq +import matplotlib.pyplot as plt +import plotly.graph_objects as go +from cadquery import exporters + +import paramak +from paramak.neutronics_utils import (add_stl_to_moab_core, + define_moab_core_and_tags) +from paramak.utils import get_hash + + +class Reactor: + """The Reactor object allows shapes and components to be added and then + collective operations to be performed on them. Combining all the shapes is + required for creating images of the whole reactor and creating a Graveyard + (bounding box) that is needed for neutronics simulations. + + Args: + shapes_and_components (list): list of paramak.Shape + """ + + def __init__(self, shapes_and_components): + + self.material_tags = [] + self.stp_filenames = [] + self.stl_filenames = [] + self.tet_meshes = [] + self.graveyard = None + self.solid = None + + self.shapes_and_components = shapes_and_components + self.reactor_hash_value = None + + self.graveyard_offset = None # set by the make_graveyard method + + @property + def stp_filenames(self): + values = [] + for shape_or_component in self.shapes_and_components: + values.append(shape_or_component.stp_filename) + return values + + @stp_filenames.setter + def stp_filenames(self, value): + self._stp_filenames = value + + @property + def stl_filenames(self): + values = [] + for shape_or_component in self.shapes_and_components: + values.append(shape_or_component.stl_filename) + return values + + @stl_filenames.setter + def stl_filenames(self, value): + self._stl_filenames = value + + @property + def largest_dimension(self): + """Calculates a bounding box for the Reactor and returns the largest + absolute value of the largest dimension of the bounding box""" + largest_dimension = 0 + for component in self.shapes_and_components: + print(component.stp_filename, component.largest_dimension) + largest_dimension = max( + largest_dimension, + component.largest_dimension) + self._largest_dimension = largest_dimension + return largest_dimension + + @largest_dimension.setter + def largest_dimension(self, value): + self._largest_dimension = value + + @property + def material_tags(self): + """Returns a set of all the materials_tags used in the Reactor + (excluding the plasma)""" + values = [] + for shape_or_component in self.shapes_and_components: + if isinstance( + shape_or_component, + (paramak.Plasma, + paramak.PlasmaFromPoints, + paramak.PlasmaBoundaries)) is False: + values.append(shape_or_component.material_tag) + return values + + @material_tags.setter + def material_tags(self, value): + self._material_tags = value + + @property + def tet_meshes(self): + values = [] + for shape_or_componet in self.shapes_and_components: + values.append(shape_or_componet.tet_mesh) + return values + + @tet_meshes.setter + def tet_meshes(self, value): + self._tet_meshes = value + + @property + def shapes_and_components(self): + """Adds a list of parametric shape(s) and or parametric component(s) + to the Reactor object. This allows collective operations to be + performed on all the shapes in the reactor. When adding a shape or + component the stp_filename of the shape or component should be unique""" + if hasattr(self, "create_solids"): + ignored_keys = ["reactor_hash_value"] + if get_hash(self, ignored_keys) != self.reactor_hash_value: + self.create_solids() + self.reactor_hash_value = get_hash(self, ignored_keys) + return self._shapes_and_components + + @shapes_and_components.setter + def shapes_and_components(self, value): + if not isinstance(value, Iterable): + raise ValueError("shapes_and_components must be a list") + self._shapes_and_components = value + + @property + def graveyard_offset(self): + return self._graveyard_offset + + @graveyard_offset.setter + def graveyard_offset(self, value): + if value is None: + self._graveyard_offset = None + elif not isinstance(value, (float, int)): + raise ValueError("graveyard_offset must be a number") + elif value < 0: + raise ValueError("graveyard_offset must be positive") + self._graveyard_offset = value + + @property + def solid(self): + """This combines all the parametric shapes and compents in the reactor + object and rotates the viewing angle so that .solid operations in + jupyter notebook. + """ + + list_of_cq_vals = [] + + for shape_or_compound in self.shapes_and_components: + if isinstance( + shape_or_compound.solid, + cq.occ_impl.shapes.Compound): + for solid in shape_or_compound.solid.Solids(): + list_of_cq_vals.append(solid) + else: + list_of_cq_vals.append(shape_or_compound.solid.val()) + + compound = cq.Compound.makeCompound(list_of_cq_vals) + + compound = compound.rotate( + startVector=(0, 1, 0), endVector=(0, 0, 1), angleDegrees=180 + ) + return compound + + @solid.setter + def solid(self, value): + self._solid = value + + def neutronics_description(self, include_plasma=False, + include_graveyard=True + ): + """A description of the reactor containing material tags, stp filenames, + and tet mesh instructions. This is used for neutronics simulations which + require linkage between volumes, materials and identification of which + volumes to tet mesh. The plasma geometry is not included by default as + it is typically not included in neutronics simulations. The reason for + this is that the low number density results in minimal interaction with + neutrons. However, it can be added if the include_plasma argument is set + to True. + + Returns: + dictionary: a dictionary of materials and filenames for the reactor + """ + + neutronics_description = [] + + for entry in self.shapes_and_components: + + if include_plasma is False and isinstance( + entry, + (paramak.Plasma, + paramak.PlasmaFromPoints, + paramak.PlasmaBoundaries)) is True: + continue + + if entry.stp_filename is None: + raise ValueError( + "Set Shape.stp_filename for all the \ + Reactor entries before using this method" + ) + + if entry.material_tag is None: + raise ValueError( + "set Shape.material_tag for all the \ + Reactor entries before using this method" + ) + + neutronics_description.append(entry.neutronics_description()) + + # This add the neutronics description for the graveyard which is unique + # as it is automatically calculated instead of being added by the user. + # Also the graveyard must have 'Graveyard' as the material name + if include_graveyard is True: + self.make_graveyard() + neutronics_description.append( + self.graveyard.neutronics_description()) + + return neutronics_description + + def export_neutronics_description( + self, + filename="manifest.json", + include_plasma=False, + include_graveyard=True): + """ + Saves Reactor.neutronics_description to a json file. The resulting json + file contains a list of dictionaries. Each dictionary entry comprises + of a material and a filename and optionally a tet_mesh instruction. The + json file can then be used with the neutronics workflows to create a + neutronics model. Creating of the neutronics model requires linkage + between volumes, materials and identification of which volumes to + tet_mesh. If the filename does not end with .json then .json will be + added. The plasma geometry is not included by default as it is + typically not included in neutronics simulations. The reason for this + is that the low number density results in minimal interactions with + neutrons. However, the plasma can be added if the include_plasma + argument is set to True. + + Args: + filename (str, optional): the filename used to save the neutronics + description + include_plasma (Boolean, optional): should the plasma be included. + Defaults to False as the plasma volume and material has very + little impact on the neutronics results due to the low density. + Including the plasma does however slow down the simulation. + include_graveyard (Boolean, optional): should the graveyard be + included. Defaults to True as this is needed for DAGMC models. + """ + + path_filename = Path(filename) + + if path_filename.suffix != ".json": + path_filename = path_filename.with_suffix(".json") + + path_filename.parents[0].mkdir(parents=True, exist_ok=True) + + with open(path_filename, "w") as outfile: + json.dump( + self.neutronics_description( + include_plasma=include_plasma, + include_graveyard=include_graveyard, + ), + outfile, + indent=4, + ) + + print("saved geometry description to ", path_filename) + + return str(path_filename) + + def export_stp(self, output_folder="", graveyard_offset=100): + """Writes stp files (CAD geometry) for each Shape object in the reactor + and the graveyard. + + Args: + output_folder (str): the folder for saving the stp files to + graveyard_offset (float, optional): the offset between the largest + edge of the geometry and inner bounding shell created. Defaults + to 100. + Returns: + list: a list of stp filenames created + """ + + if len(self.stp_filenames) != len(set(self.stp_filenames)): + raise ValueError( + "Set Reactor already contains a shape or component \ + with this stp_filename", + self.stp_filenames, + ) + + filenames = [] + for entry in self.shapes_and_components: + if entry.stp_filename is None: + raise ValueError( + "set .stp_filename property for \ + Shapes before using the export_stp method" + ) + filenames.append( + str(Path(output_folder) / Path(entry.stp_filename))) + entry.export_stp(Path(output_folder) / Path(entry.stp_filename)) + + # creates a graveyard (bounding shell volume) which is needed for + # nuetronics simulations + self.make_graveyard(graveyard_offset=graveyard_offset) + filenames.append( + str(Path(output_folder) / Path(self.graveyard.stp_filename))) + self.graveyard.export_stp( + Path(output_folder) / Path(self.graveyard.stp_filename) + ) + + return filenames + + def export_stl(self, output_folder="", tolerance=0.001): + """Writes stl files (CAD geometry) for each Shape object in the reactor + + :param output_folder: the folder for saving the stp files to + :type output_folder: str + :param tolerance: the precision of the faceting + :type tolerance: float + + :return: a list of stl filenames created + :rtype: list + """ + + if len(self.stl_filenames) != len(set(self.stl_filenames)): + raise ValueError( + "Set Reactor already contains a shape or component \ + with this stl_filename", + self.stl_filenames, + ) + + filenames = [] + for entry in self.shapes_and_components: + print("entry.stl_filename", entry.stl_filename) + if entry.stl_filename is None: + raise ValueError( + "set .stl_filename property for \ + Shapes before using the export_stl method" + ) + + filenames.append( + str(Path(output_folder) / Path(entry.stl_filename))) + entry.export_stl( + Path(output_folder) / + Path( + entry.stl_filename), + tolerance) + + # creates a graveyard (bounding shell volume) which is needed for + # nuetronics simulations + self.make_graveyard() + filenames.append( + str(Path(output_folder) / Path(self.graveyard.stl_filename))) + self.graveyard.export_stl( + Path(output_folder) / Path(self.graveyard.stl_filename) + ) + + print("exported stl files ", filenames) + + return filenames + + def export_h5m( + self, + filename='dagmc.h5m', + skip_graveyard=False, + tolerance=0.001, + graveyard_offset=100): + """Converts stl files into DAGMC compatible h5m file using PyMOAB. The + DAGMC file produced has not been imprinted and merged unlike the other + supported method which uses Trelis to produce an imprinted and merged + DAGMC geometry. If the provided filename doesn't end with .h5m it will + be added + + Args: + filename (str, optional): filename of h5m outputfile + Defaults to "dagmc.h5m". + skip_graveyard (boolean, optional): filename of h5m outputfile + Defaults to False. + tolerance (float, optional): the precision of the faceting + Defaults to 0.001. + graveyard_offset (float, optional): the offset between the largest + edge of the geometry and inner bounding shell created. Defaults + to 100. + Returns: + filename: output h5m filename + """ + + path_filename = Path(filename) + + if path_filename.suffix != ".h5m": + path_filename = path_filename.with_suffix(".h5m") + + path_filename.parents[0].mkdir(parents=True, exist_ok=True) + + moab_core, moab_tags = define_moab_core_and_tags() + + surface_id = 1 + volume_id = 1 + + for item in self.shapes_and_components: + + item.export_stl(item.stl_filename, tolerance=tolerance) + moab_core = add_stl_to_moab_core( + moab_core, + surface_id, + volume_id, + item.material_tag, + moab_tags, + item.stl_filename) + volume_id += 1 + surface_id += 1 + + if skip_graveyard is False: + self.make_graveyard(graveyard_offset=graveyard_offset) + self.graveyard.export_stl(self.graveyard.stl_filename) + volume_id = 2 + surface_id = 2 + moab_core = add_stl_to_moab_core( + moab_core, + surface_id, + volume_id, + self.graveyard.material_tag, + moab_tags, + self.graveyard.stl_filename + ) + + all_sets = moab_core.get_entities_by_handle(0) + + file_set = moab_core.create_meshset() + + moab_core.add_entities(file_set, all_sets) + + moab_core.write_file(str(path_filename)) + + return filename + + def export_physical_groups(self, output_folder=""): + """Exports several JSON files containing a look up table which is + useful for identifying faces and volumes. The output file names are + generated from .stp_filename properties. + + Args: + output_folder (str, optional): directory of outputfiles. + Defaults to "". + + Raises: + ValueError: if one .stp_filename property is set to None + + Returns: + list: list of output file names + """ + filenames = [] + for entry in self.shapes_and_components: + if entry.stp_filename is None: + raise ValueError( + "set .stp_filename property for \ + Shapes before using the export_stp method" + ) + filenames.append( + str(Path(output_folder) / Path(entry.stp_filename))) + entry.export_physical_groups( + Path(output_folder) / Path(entry.stp_filename)) + return filenames + + def export_svg(self, filename): + """Exports an svg file for the Reactor.solid. If the filename provided + doesn't end with .svg it will be added. + + Args: + filename (str): the filename of the svg file to be exported + """ + + path_filename = Path(filename) + + if path_filename.suffix != ".svg": + path_filename = path_filename.with_suffix(".svg") + + path_filename.parents[0].mkdir(parents=True, exist_ok=True) + + with open(path_filename, "w") as out_file: + exporters.exportShape(self.solid, "SVG", out_file) + print("Saved file as ", path_filename) + + def export_graveyard( + self, + graveyard_offset=100, + filename="Graveyard.stp"): + """Writes an stp file (CAD geometry) for the reactor graveyard. This + is needed for DAGMC simulations. This method also calls + Reactor.make_graveyard with the offset. + + Args: + filename (str): the filename for saving the stp file + graveyard_offset (float): the offset between the largest edge of + the geometry and inner bounding shell created. Defaults to + Reactor.graveyard_offset + + Returns: + str: the stp filename created + """ + + self.make_graveyard(graveyard_offset=graveyard_offset) + self.graveyard.export_stp(Path(filename)) + + return filename + + def make_graveyard(self, graveyard_offset=100): + """Creates a graveyard volume (bounding box) that encapsulates all + volumes. This is required by DAGMC when performing neutronics + simulations. + + Args: + graveyard_offset (float): the offset between the largest edge of + the geometry and inner bounding shell created. Defaults to + Reactor.graveyard_offset + + Returns: + CadQuery solid: a shell volume that bounds the geometry, referred + to as a graveyard in DAGMC + """ + + self.graveyard_offset = graveyard_offset + + for component in self.shapes_and_components: + if component.solid is None: + component.create_solid() + + graveyard_shape = paramak.HollowCube( + length=self.largest_dimension * 2 + graveyard_offset * 2, + name="Graveyard", + material_tag="Graveyard", + stp_filename="Graveyard.stp", + stl_filename="Graveyard.stl", + ) + + self.graveyard = graveyard_shape + + return graveyard_shape + + def export_2d_image( + self, + filename="2d_slice.png", + xmin=0.0, + xmax=900.0, + ymin=-600.0, + ymax=600.0): + """Creates a 2D slice image (png) of the reactor. + + Args: + filename (str): output filename of the image created + + Returns: + str: png filename created + """ + + path_filename = Path(filename) + + if path_filename.suffix != ".png": + path_filename = path_filename.with_suffix(".png") + + path_filename.parents[0].mkdir(parents=True, exist_ok=True) + + fig, ax = plt.subplots() + + # creates indvidual patches for each Shape which are combined together + for entry in self.shapes_and_components: + patch = entry._create_patch() + ax.add_collection(patch) + + ax.axis("equal") + ax.set(xlim=(xmin, xmax), ylim=(ymin, ymax)) + ax.set_aspect("equal", "box") + + Path(filename).parent.mkdir(parents=True, exist_ok=True) + plt.savefig(filename, dpi=100) + plt.close() + + print("\n saved 2d image to ", str(path_filename)) + + return str(path_filename) + + def export_html(self, filename="reactor.html"): + """Creates a html graph representation of the points for the Shape + objects that make up the reactor. Note, If filename provided doesn't end + with .html then it will be appended. + + Args: + filename (str): the filename to save the html graph + + Returns: + plotly figure: figure object + """ + + path_filename = Path(filename) + + if path_filename.suffix != ".html": + path_filename = path_filename.with_suffix(".html") + + path_filename.parents[0].mkdir(parents=True, exist_ok=True) + + fig = go.Figure() + fig.update_layout( + {"title": "coordinates of components", "hovermode": "closest"} + ) + + # accesses the Shape traces for each Shape and adds them to the figure + for entry in self.shapes_and_components: + fig.add_trace(entry._trace()) + + fig.write_html(str(path_filename)) + print("Exported html graph to ", str(path_filename)) + + return fig diff --git a/paramak/shape.py b/paramak/shape.py new file mode 100644 index 000000000..5b508b926 --- /dev/null +++ b/paramak/shape.py @@ -0,0 +1,1210 @@ + +import json +import numbers +import warnings +from collections import Iterable +from os import fdopen, remove +from pathlib import Path +from shutil import copymode, move +from tempfile import mkstemp + +import cadquery as cq +import matplotlib.pyplot as plt +import plotly.graph_objects as go +from cadquery import exporters +from matplotlib.collections import PatchCollection +from matplotlib.patches import Polygon + +import paramak +from paramak.neutronics_utils import (add_stl_to_moab_core, + define_moab_core_and_tags) +from paramak.utils import cut_solid, get_hash, intersect_solid, union_solid + + +class Shape: + """A shape object that represents a 3d volume and can have materials and + neutronics tallies assigned. Shape objects are not intended to be used + directly bly the user but provide basic functionality for user-facing + classes that inherit from Shape. + + Args: + points (list of (float, float, float), optional): the x, y, z + coordinates of points that make up the shape. Defaults to None. + connection_type (str, optional): The type of connection between points. + Possible values are "straight", "circle", "spline", "mixed". + Defaults to "mixed". + name (str, optional): the name of the shape, used in the graph legend + by export_html. Defaults to None. + color ((float, float, float [, float]), optional): The color to use when exporting as html + graphs or png images. Can be in RGB or RGBA format with floats + between 0 and 1. Defaults to (0.5, 0.5, 0.5). + material_tag (str, optional): the material name to use when exporting + the neutronics description. Defaults to None. + stp_filename (str, optional): the filename used when saving stp files. + Defaults to None. + stl_filename (str, optional): the filename used when saving stl files. + Defaults to None. + azimuth_placement_angle (iterable of floats or float, optional): the + azimuth angle(s) used when positioning the shape. If a list of + angles is provided, the shape is duplicated at all angles. + Defaults to 0.0. + workplane (str, optional): the orientation of the Cadquery workplane. + (XY, YZ or XZ). Defaults to "XZ". + rotation_axis (str or list, optional): rotation axis around which the + solid is rotated. If None, the rotation axis will depend on the + workplane or path_workplane if applicable. Can be set to "X", "-Y", + "Z", etc. A custom axis can be set by setting a list of two XYZ + floats. Defaults to None. + tet_mesh (str, optional): If not None, a tet mesh flag will be added to + the neutronics description output. Defaults to None. + physical_groups (dict, optional): contains information on physical + groups (volumes and surfaces). Defaults to None. + cut (paramak.shape or list, optional): If set, the current solid will + be cut with the provided solid or iterable in cut. Defaults to + None. + intersect (paramak.shape or list, optional): If set, the current solid + will be interested with the provided solid or iterable of solids. + Defaults to None. + union (paramak.shape or list, optional): If set, the current solid + will be united with the provided solid or iterable of solids. + Defaults to None. + """ + + def __init__( + self, + points=None, + connection_type="mixed", + name=None, + color=(0.5, 0.5, 0.5), + material_tag=None, + stp_filename=None, + stl_filename=None, + azimuth_placement_angle=0.0, + workplane="XZ", + rotation_axis=None, + tet_mesh=None, + physical_groups=None, + cut=None, + intersect=None, + union=None, + ): + + self.connection_type = connection_type + self.points = points + self.stp_filename = stp_filename + self.stl_filename = stl_filename + self.color = color + self.name = name + + self.cut = cut + self.intersect = intersect + self.union = union + + self.azimuth_placement_angle = azimuth_placement_angle + self.workplane = workplane + self.rotation_axis = rotation_axis + + # neutronics specific properties + self.material_tag = material_tag + self.tet_mesh = tet_mesh + + self.physical_groups = physical_groups + + # properties calculated internally by the class + self.solid = None + self.render_mesh = None + # self.volume = None + self.hash_value = None + self.points_hash_value = None + self.x_min = None + self.x_max = None + self.z_min = None + self.z_max = None + self.graveyard_offset = None # set by the make_graveyard method + self.patch = None + + @property + def solid(self): + """The CadQuery solid of the 3d object. Returns a CadQuery workplane + or CadQuery Compound""" + + ignored_keys = ["_solid", "_hash_value"] + if get_hash(self, ignored_keys) != self.hash_value: + self.create_solid() + self.hash_value = get_hash(self, ignored_keys) + + return self._solid + + @solid.setter + def solid(self, value): + self._solid = value + + @property + def cut(self): + return self._cut + + @cut.setter + def cut(self, value): + self._cut = value + + @property + def intersect(self): + return self._intersect + + @intersect.setter + def intersect(self, value): + self._intersect = value + + @property + def union(self): + return self._union + + @union.setter + def union(self, value): + self._union = value + + @property + def largest_dimension(self): + """Calculates a bounding box for the Shape and returns the largest + absolute value of the largest dimension of the bounding box""" + largest_dimension = 0 + if isinstance(self.solid, cq.Compound): + for solid in self.solid.Solids(): + largest_dimension = max( + abs(self.solid.BoundingBox().xmax), + abs(self.solid.BoundingBox().xmin), + abs(self.solid.BoundingBox().ymax), + abs(self.solid.BoundingBox().ymin), + abs(self.solid.BoundingBox().zmax), + abs(self.solid.BoundingBox().zmin), + largest_dimension + ) + else: + largest_dimension = max( + abs(self.solid.val().BoundingBox().xmax), + abs(self.solid.val().BoundingBox().xmin), + abs(self.solid.val().BoundingBox().ymax), + abs(self.solid.val().BoundingBox().ymin), + abs(self.solid.val().BoundingBox().zmax), + abs(self.solid.val().BoundingBox().zmin), + largest_dimension + ) + self.largest_dimension = largest_dimension + return largest_dimension + + @largest_dimension.setter + def largest_dimension(self, value): + self._largest_dimension = value + + @property + def workplane(self): + return self._workplane + + @workplane.setter + def workplane(self, value): + acceptable_values = ["XY", "YZ", "XZ", "YX", "ZY", "ZX"] + if value in acceptable_values: + self._workplane = value + else: + raise ValueError( + "Shape.workplane must be one of ", + acceptable_values, + " not ", + value) + + @property + def rotation_axis(self): + return self._rotation_axis + + @rotation_axis.setter + def rotation_axis(self, value): + if isinstance(value, str): + acceptable_values = \ + ["X", "Y", "Z", "-X", "-Y", "-Z", "+X", "+Y", "+Z"] + if value not in acceptable_values: + msg = "Shape.rotation_axis must be one of " + \ + " ".join(acceptable_values) + \ + " not " + value + raise ValueError(msg) + elif isinstance(value, Iterable): + msg = "Shape.rotation_axis must be a list of two (X, Y, Z) floats" + if len(value) != 2: + raise ValueError(msg) + for point in value: + if not isinstance(point, tuple): + raise ValueError(msg) + if len(point) != 3: + raise ValueError(msg) + for val in point: + if not isinstance(val, (int, float)): + raise ValueError(msg) + + if value[0] == value[1]: + msg = "The two points must be different" + raise ValueError(msg) + elif value is not None: + msg = "Shape.rotation_axis must be a list or a string or None" + raise ValueError(msg) + self._rotation_axis = value + + @property + def volume(self): + """Get the total volume of the Shape. Returns a float""" + if isinstance(self.solid, cq.Compound): + return self.solid.Volume() + + return self.solid.val().Volume() + + @property + def volumes(self): + """Get the volumes of the Shape. Compound shapes provide a seperate + volume value for each entry. Returns a list of floats""" + all_volumes = [] + if isinstance(self.solid, cq.Compound): + for solid in self.solid.Solids(): + all_volumes.append(solid.Volume()) + return all_volumes + + return [self.solid.val().Volume()] + + @property + def area(self): + """Get the total surface area of the Shape. Returns a float""" + if isinstance(self.solid, cq.Compound): + return self.solid.Area() + + return self.solid.val().Area() + + @property + def areas(self): + """Get the surface areas of the Shape. Compound shapes provide a + seperate area value for each entry. Returns a list of floats""" + all_areas = [] + if isinstance(self.solid, cq.Compound): + for face in self.solid.Faces(): + all_areas.append(face.Area()) + return all_areas + + for face in self.solid.val().Faces(): + all_areas.append(face.Area()) + return all_areas + + @property + def hash_value(self): + return self._hash_value + + @hash_value.setter + def hash_value(self, value): + self._hash_value = value + + @property + def points_hash_value(self): + return self._points_hash_value + + @points_hash_value.setter + def points_hash_value(self, value): + self._points_hash_value = value + + @property + def color(self): + return self._color + + @color.setter + def color(self, value): + error = False + if isinstance(value, (list, tuple)): + if len(value) in [3, 4]: + for i in value: + if not isinstance(i, (int, float)): + error = True + else: + error = True + else: + error = True + # raise error + if error: + raise ValueError( + "Shape.color must be a list or tuple of 3 or 4 floats") + self._color = value + + @property + def material_tag(self): + """The material_tag assigned to the Shape. Used when taging materials + for use in neutronics descriptions""" + + return self._material_tag + + @material_tag.setter + def material_tag(self, value): + if value is None: + self._material_tag = value + elif isinstance(value, str): + if len(value) > 27: + msg = "Shape.material_tag > 28 characters." + \ + "Use with DAGMC will be affected." + str(value) + warnings.warn(msg, UserWarning) + self._material_tag = value + else: + raise ValueError("Shape.material_tag must be a string", value) + + @property + def tet_mesh(self): + return self._tet_mesh + + @tet_mesh.setter + def tet_mesh(self, value): + if value is not None and not isinstance(value, str): + raise ValueError("Shape.tet_mesh must be a string", value) + self._tet_mesh = value + + @property + def name(self): + """The name of the Shape, used to identify Shapes when exporting_html + """ + return self._name + + @name.setter + def name(self, value): + if value is not None and not isinstance(value, str): + raise ValueError("Shape.name must be a string", value) + self._name = value + + @property + def points(self): + """Sets the Shape.point attributes. + + Args: + points (a list of lists or tuples): list of points that create the + shape + + Raises: + incorrect type: only list of lists or tuples are accepted + """ + ignored_keys = ["_points", "_points_hash_value"] + if hasattr(self, 'find_points') and \ + self.points_hash_value != get_hash(self, ignored_keys): + self.find_points() + self.points_hash_value = get_hash(self, ignored_keys) + + return self._points + + @points.setter + def points(self, values): + + if values is not None: + if not isinstance(values, list): + raise ValueError("points must be a list") + + for value in values: + if type(value) not in [list, tuple]: + msg = "individual points must be a list or a tuple." + \ + "{} in of type {}".format(value, type(value)) + raise ValueError(msg) + + for value in values: + # Checks that the length of each tuple in points is 2 or 3 + if len(value) not in [2, 3]: + msg = "individual points contain 2 or 3 entries {} has a \ + length of {}".format(value, len(values[0])) + raise ValueError(msg) + + # Checks that the XY points are numbers + if not isinstance(value[0], numbers.Number): + msg = "The first value in the tuples that make \ + up the points represents the X value \ + and must be a number {}".format(value) + raise ValueError(msg) + if not isinstance(value[1], numbers.Number): + msg = "The second value in the tuples that make \ + up the points represents the X value \ + and must be a number {}".format(value) + raise ValueError(msg) + + # Checks that only straight and spline are in the connections + # part of points + if len(value) == 3: + if value[2] not in ["straight", "spline", "circle"]: + msg = 'individual connections must be either \ + "straight", "circle" or "spline"' + raise ValueError(msg) + + # checks that the entries in the points are either all 2 long or + # all 3 long, not a mixture + if not all(len(entry) == 2 for entry in values): + if not all(len(entry) == 3 for entry in values): + msg = "The points list should contain entries of length 2 \ + or 3 but not a mixture of 2 and 3" + raise ValueError(msg) + + if len(values) > 1: + if values[0][:2] == values[-1][:2]: + msg = "The coordinates of the last and first points are \ + the same." + raise ValueError(msg) + + values.append(values[0]) + if self.connection_type != "mixed": + values = [(*p, self.connection_type) for p in values] + + self._points = values + + @property + def stp_filename(self): + """Sets the Shape.stp_filename attribute which is used as the filename + when exporting the geometry to stp format. Note, .stp will be added to + filenames not ending with .step or .stp. + + Args: + value (str): the value to use as the stp_filename + + Raises: + incorrect type: only str values are accepted + """ + + return self._stp_filename + + @stp_filename.setter + def stp_filename(self, value): + if value is not None: + if isinstance(value, str): + if Path(value).suffix not in [".stp", ".step"]: + msg = "Incorrect filename ending, filename must end with \ + .stp or .step" + raise ValueError(msg) + else: + msg = "stp_filename must be a \ + string {} {}".format(value, type(value)) + raise ValueError(msg) + self._stp_filename = value + + @property + def stl_filename(self): + """Sets the Shape.stl_filename attribute which is used as the filename + when exporting the geometry to stl format. Note .stl will be added to + filenames not ending with .stl + + Args: + value (str): the value to use as the stl_filename + + Raises: + incorrect type: only str values are accepted + """ + return self._stl_filename + + @stl_filename.setter + def stl_filename(self, value): + if value is not None: + if isinstance(value, str): + if Path(value).suffix != ".stl": + msg = "Incorrect filename ending, filename must end with \ + .stl" + raise ValueError(msg) + else: + msg = "stl_filename must be a string \ + {} {}".format(value, type(value)) + raise ValueError(msg) + self._stl_filename = value + + @property + def azimuth_placement_angle(self): + return self._azimuth_placement_angle + + @azimuth_placement_angle.setter + def azimuth_placement_angle(self, value): + error = False + if isinstance(value, (int, float, Iterable)) and \ + not isinstance(value, str): + if isinstance(value, Iterable): + for i in value: + if not isinstance(i, (int, float)): + error = True + else: + error = True + + if error: + msg = "azimuth_placement_angle must be a float or list of floats" + raise ValueError(msg) + self._azimuth_placement_angle = value + + def create_solid(self): + solid = None + if self.points is not None: + # obtains the first two values of the points list + XZ_points = [(p[0], p[1]) for p in self.points] + + # obtains the last values of the points list + connections = [p[2] for p in self.points[:-1]] + + current_linetype = connections[0] + current_points_list = [] + instructions = [] + # groups together common connection types + for i, connection in enumerate(connections): + if connection == current_linetype: + current_points_list.append(XZ_points[i]) + else: + current_points_list.append(XZ_points[i]) + instructions.append( + {current_linetype: current_points_list}) + current_linetype = connection + current_points_list = [XZ_points[i]] + instructions.append({current_linetype: current_points_list}) + + if list(instructions[-1].values())[0][-1] != XZ_points[0]: + keyname = list(instructions[-1].keys())[0] + instructions[-1][keyname].append(XZ_points[0]) + + if hasattr(self, "path_points"): + + factor = 1 + if self.workplane in ["XZ", "YX", "ZY"]: + factor *= -1 + + solid = cq.Workplane(self.workplane).moveTo(0, 0) + + if self.force_cross_section: + for point in self.path_points[:-1]: + solid = solid.workplane( + offset=point[1] * + factor).moveTo( + point[0], + 0).workplane() + for entry in instructions: + if list(entry.keys())[0] == "spline": + solid = solid.spline( + listOfXYTuple=list(entry.values())[0]) + if list(entry.keys())[0] == "straight": + solid = solid.polyline(list(entry.values())[0]) + if list(entry.keys())[0] == "circle": + p0 = list(entry.values())[0][0] + p1 = list(entry.values())[0][1] + p2 = list(entry.values())[0][2] + solid = solid.moveTo( + p0[0], p0[1]).threePointArc( + p1, p2) + solid = solid.close().moveTo( + 0, 0).moveTo(-point[0], 0).workplane(offset=-point[1] * factor) + + elif self.force_cross_section == False: + solid = solid.workplane( + offset=self.path_points[0][1] * + factor).moveTo( + self.path_points[0][0], + 0).workplane() + for entry in instructions: + if list(entry.keys())[0] == "spline": + solid = solid.spline( + listOfXYTuple=list(entry.values())[0]) + if list(entry.keys())[0] == "straight": + solid = solid.polyline(list(entry.values())[0]) + if list(entry.keys())[0] == "circle": + p0 = list(entry.values())[0][0] + p1 = list(entry.values())[0][1] + p2 = list(entry.values())[0][2] + solid = solid.moveTo( + p0[0], p0[1]).threePointArc( + p1, p2) + + solid = solid.close().moveTo(0, + 0).moveTo(-self.path_points[0][0], + 0).workplane(offset=-self.path_points[0][1] * factor) + + solid = solid.workplane( + offset=self.path_points[-1][1] * factor).moveTo(self.path_points[-1][0], 0).workplane() + + else: + # for rotate and extrude shapes + solid = cq.Workplane(self.workplane) + # for extrude shapes + if hasattr(self, "extrusion_start_offset"): + extrusion_offset = -self.extrusion_start_offset + solid = solid.workplane(offset=extrusion_offset) + + for entry in instructions: + if list(entry.keys())[0] == "spline": + solid = solid.spline(listOfXYTuple=list(entry.values())[0]) + if list(entry.keys())[0] == "straight": + solid = solid.polyline(list(entry.values())[0]) + if list(entry.keys())[0] == "circle": + p0 = list(entry.values())[0][0] + p1 = list(entry.values())[0][1] + p2 = list(entry.values())[0][2] + solid = solid.moveTo(p0[0], p0[1]).threePointArc(p1, p2) + + return solid + + def rotate_solid(self, solid): + # Checks if the azimuth_placement_angle is a list of angles + if isinstance(self.azimuth_placement_angle, Iterable): + azimuth_placement_angles = self.azimuth_placement_angle + else: + azimuth_placement_angles = [self.azimuth_placement_angle] + + rotated_solids = [] + # Perform seperate rotations for each angle + for angle in azimuth_placement_angles: + rotated_solids.append( + solid.rotate( + *self.get_rotation_axis()[0], angle)) + solid = cq.Workplane(self.workplane) + + # Joins the seperate solids together + for i in rotated_solids: + solid = solid.union(i) + return solid + + def get_rotation_axis(self): + """Returns the rotation axis for a given shape. If self.rotation_axis + is None, the rotation axis will be computed from self.workplane (or + from self.path_workplane if applicable). If self.rotation_axis is an + acceptable string (eg. "X", "+Y", "-Z"...) then this axis will be used. + If self.rotation_axis is a list of two points, then these two points + will be used to form an axis. + + Returns: + list, str: list of two XYZ points and the string of the axis (eg. + "X", "Y"..) + """ + rotation_axis = { + "X": [(-1, 0, 0), (1, 0, 0)], + "-X": [(1, 0, 0), (-1, 0, 0)], + "Y": [(0, -1, 0), (0, 1, 0)], + "-Y": [(0, 1, 0), (0, -1, 0)], + "Z": [(0, 0, -1), (0, 0, 1)], + "-Z": [(0, 0, 1), (0, 0, -1)], + } + if isinstance(self.rotation_axis, str): + # X, Y or Z axis + return ( + rotation_axis[self.rotation_axis.replace("+", "")], + self.rotation_axis + ) + elif isinstance(self.rotation_axis, Iterable): + # Custom axis + return self.rotation_axis, "custom_axis" + elif self.rotation_axis is None: + # Axis from workplane or path_workplane + if hasattr(self, "path_workplane"): + # compute from path_workplane instead + workplane = self.path_workplane + else: + workplane = self.workplane + return rotation_axis[workplane[1]], workplane[1] + + def create_limits(self): + """Finds the x,y,z limits (min and max) of the points that make up the + face of the shape. Note the Shape may extend beyond this boundary if + splines are used to connect points. + + Raises: + ValueError: if no points are defined + + Returns: + float, float, float, float, float, float: x_minimum, x_maximum, + y_minimum, y_maximum, z_minimum, z_maximum + """ + + if hasattr(self, "find_points"): + self.find_points() + if self.points is None: + raise ValueError("No points defined for", self) + + self.x_min = float(min([row[0] for row in self.points])) + self.x_max = float(max([row[0] for row in self.points])) + + self.z_min = float(min([row[1] for row in self.points])) + self.z_max = float(max([row[1] for row in self.points])) + + return self.x_min, self.x_max, self.z_min, self.z_max + + def export_stl(self, filename, tolerance=0.001): + """Exports an stl file for the Shape.solid. If the provided filename + doesn't end with .stl it will be added + + Args: + filename (str): the filename of the stl file to be exported + tolerance (float): the precision of the faceting + """ + + path_filename = Path(filename) + + if path_filename.suffix != ".stl": + path_filename = path_filename.with_suffix(".stl") + + path_filename.parents[0].mkdir(parents=True, exist_ok=True) + + with open(path_filename, "w") as out_file: + exporters.exportShape(self.solid, "STL", out_file, tolerance) + print("Saved file as ", path_filename) + + return str(path_filename) + + def export_stp(self, filename=None, units='mm'): + """Exports an stp file for the Shape.solid. If the filename provided + doesn't end with .stp or .step then .stp will be added. If a + filename is not provided and the shape's stp_filename property is + not None the stp_filename will be used as the export filename. + + Args: + filename (str): the filename of the stp + units (str): the units of the stp file, options are 'cm' or 'mm'. + Default is mm. + """ + + if filename is not None: + path_filename = Path(filename) + + if path_filename.suffix == ".stp" or path_filename.suffix == ".step": + pass + else: + path_filename = path_filename.with_suffix(".stp") + + path_filename.parents[0].mkdir(parents=True, exist_ok=True) + elif self.stp_filename is not None: + path_filename = Path(self.stp_filename) + + with open(path_filename, "w") as out_file: + exporters.exportShape(self.solid, "STEP", out_file) + + if units == 'cm': + self._replace( + path_filename, + 'SI_UNIT(.MILLI.,.METRE.)', + 'SI_UNIT(.CENTI.,.METRE.)') + + print("Saved file as ", path_filename) + + return str(path_filename) + + def export_physical_groups(self, filename): + """Exports a JSON file containing a look up table which is useful for + identifying faces and volumes. If filename provided doesn't end with + .json then .json will be added. + + Args: + filename (str): the filename used to save the json file + """ + + path_filename = Path(filename) + + if path_filename.suffix != ".json": + path_filename = path_filename.with_suffix(".json") + + path_filename.parents[0].mkdir(parents=True, exist_ok=True) + if self.physical_groups is not None: + with open(filename, "w") as outfile: + json.dump(self.physical_groups, outfile, indent=4) + + print("Saved physical_groups description to ", path_filename) + else: + print( + "Warning: physical_groups attribute is None \ + for {}".format( + self.name + ) + ) + + return filename + + def export_svg(self, filename): + """Exports an svg file for the Shape.solid. If the provided filename + doesn't end with .svg it will be added. + + Args: + filename (str): the filename of the svg file to be exported + """ + + path_filename = Path(filename) + + if path_filename.suffix != ".svg": + path_filename = path_filename.with_suffix(".svg") + + path_filename.parents[0].mkdir(parents=True, exist_ok=True) + + with open(path_filename, "w") as out_file: + exporters.exportShape(self.solid, "SVG", out_file) + print("Saved file as ", path_filename) + + return str(path_filename) + + def export_html(self, filename): + """Creates a html graph representation of the points and connections + for the Shape object. Shapes are colored by their .color property. + Shapes are also labelled by their .name. If filename provided doesn't + end with .html then .html will be added. + + Args: + filename (str): the filename used to save the html graph + + Returns: + plotly.Figure(): figure object + """ + + if self.__class__.__name__ == "SweepCircleShape": + msg = 'WARNING: export_html will plot path_points for ' + \ + 'the SweepCircleShape class' + print(msg) + + if self.points is None: + raise ValueError("No points defined for", self) + + Path(filename).parents[0].mkdir(parents=True, exist_ok=True) + + path_filename = Path(filename) + + if path_filename.suffix != ".html": + path_filename = path_filename.with_suffix(".html") + + fig = go.Figure() + fig.update_layout( + {"title": "coordinates of components", "hovermode": "closest"} + ) + + fig.add_trace(self._trace()) + + fig.write_html(str(path_filename)) + + print("Exported html graph to ", path_filename) + + return fig + + def _trace(self): + """Creates a plotly trace representation of the points of the Shape + object. This method is intended for internal use by Shape.export_html. + + Returns: + plotly trace: trace object + """ + + color_list = [i * 255 for i in self.color] + + if len(color_list) == 3: + color = "rgb(" + str(color_list).strip("[]") + ")" + elif len(color_list) == 4: + color = "rgba(" + str(color_list).strip("[]") + ")" + + if self.name is None: + name = "Shape not named" + else: + name = self.name + + text_values = [] + + for i, point in enumerate(self.points[:-1]): + if len(point) == 3: + text_values.append( + "point number=" + + str(i) + + "
" + + "connection to next point=" + + str(point[2]) + + "
" + + "x=" + + str(point[0]) + + "
" + + "z=" + + str(point[1]) + + "
" + ) + else: + text_values.append( + "point number=" + + str(i) + + "
" + + "x=" + + str(point[0]) + + "
" + + "z=" + + str(point[1]) + + "
" + ) + + trace = go.Scatter( + { + "x": [row[0] for row in self.points], + "y": [row[1] for row in self.points], + "hoverinfo": "text", + "text": text_values, + "mode": "markers+lines", + "marker": {"size": 5, "color": color}, + "name": name, + } + ) + + return trace + + def export_2d_image( + self, filename, xmin=0., xmax=900., ymin=-600., ymax=600.): + """Exports a 2d image (png) of the reactor. Components are colored by + their Shape.color property. If filename provided doesn't end with .png + then .png will be added. + + Args: + filename (str): the filename of the saved png image. + xmin (float, optional): the minimum x value of the x axis. + Defaults to 0.. + xmax (float, optional): the maximum x value of the x axis. + Defaults to 900.. + ymin (float, optional): the minimum y value of the y axis. + Defaults to -600.. + ymax (float, optional): the maximum y value of the y axis. + Defaults to 600.. + + Returns: + matplotlib.plt(): a plt object + """ + + fig, ax = plt.subplots() + + patch = self._create_patch() + + ax.add_collection(patch) + + ax.axis("equal") + ax.set(xlim=(xmin, xmax), ylim=(ymin, ymax)) + ax.set_aspect("equal", "box") + + plt.savefig(filename, dpi=100) + plt.close() + print("\n saved 2d image to ", filename) + + return plt + + def _create_patch(self): + """Creates a matplotlib polygon patch from the Shape points. This is + used when making 2d images of the Shape object. + + Raises: + ValueError: No points defined for the Shape + + Returns: + Matplotlib object patch: a plotable polygon shape + """ + + if self.points is None: + raise ValueError("No points defined for", self) + + patches = [] + xylist = [] + + for point in self.points: + xylist.append([point[0], point[1]]) + + polygon = Polygon(xylist, closed=True) + patches.append(polygon) + + patch = PatchCollection(patches) + + if self.color is not None: + patch.set_facecolor(self.color) + patch.set_color(self.color) + patch.color = self.color + patch.edgecolor = self.color + # checks to see if an alpha value is provided in the color + if len(self.color) == 4: + patch.set_alpha = self.color[-1] + self.patch = patch + return patch + + def neutronics_description(self): + """Returns a neutronics description of the Shape object. This is needed + for the use with automated neutronics model methods which require + linkage between the stp files and materials. If tet meshing of the + volume is required then Trelis meshing commands can be optionally + specified as the tet_mesh argument. + + Returns: + dictionary: a dictionary of the step filename and material name + """ + + neutronics_description = {"material": self.material_tag} + + if self.stp_filename is not None: + neutronics_description["stp_filename"] = self.stp_filename + # this is needed as ppp looks for the filename key + neutronics_description["filename"] = self.stp_filename + + if self.tet_mesh is not None: + neutronics_description["tet_mesh"] = self.tet_mesh + + if self.stl_filename is not None: + neutronics_description["stl_filename"] = self.stl_filename + + return neutronics_description + + def perform_boolean_operations(self, solid, **kwargs): + """Performs boolean cut, intersect and union operations if shapes are + provided""" + + # If a cut solid is provided then perform a boolean cut + if self.cut is not None: + solid = cut_solid(solid, self.cut) + + # If a wedge cut is provided then perform a boolean cut + # Performed independantly to avoid use of self.cut + # Prevents repetition of 'outdated' wedge cuts + if 'wedge_cut' in kwargs: + if kwargs['wedge_cut'] is not None: + solid = cut_solid(solid, kwargs['wedge_cut']) + + # If an intersect is provided then perform a boolean intersect + if self.intersect is not None: + solid = intersect_solid(solid, self.intersect) + + # If an intersect is provided then perform a boolean intersect + if self.union is not None: + solid = union_solid(solid, self.union) + + return solid + + def _replace(self, filename, pattern, subst): + """Opens a file and replaces occurances of a particular string + (pattern)with a new string (subst) and overwrites the file. + Used internally within the paramak to ensure .STP files are + in units of cm not the default mm. + Args: + filename (str): the filename of the file to edit + pattern (str): the string that should be removed + subst (str): the string that should be used in the place of the + pattern string + """ + # Create temp file + file_handle, abs_path = mkstemp() + with fdopen(file_handle, 'w') as new_file: + with open(filename) as old_file: + for line in old_file: + new_file.write(line.replace(pattern, subst)) + + # Copy the file permissions from the old file to the new file + copymode(filename, abs_path) + + # Remove original file + remove(filename) + + # Move new file + move(abs_path, filename) + + def make_graveyard(self, graveyard_offset=100): + """Creates a graveyard volume (bounding box) that encapsulates all + volumes. This is required by DAGMC when performing neutronics + simulations. + + Args: + graveyard_offset (float): the offset between the largest edge of + the geometry and inner bounding shell created. Defaults to + 100 + + Returns: + CadQuery solid: a shell volume that bounds the geometry, referred + to as a graveyard in DAGMC + """ + + self.graveyard_offset = graveyard_offset + + if self.solid is None: + self.create_solid() + + graveyard_shape = paramak.HollowCube( + length=self.largest_dimension * 2 + graveyard_offset * 2, + name="Graveyard", + material_tag="Graveyard", + stp_filename="Graveyard.stp", + stl_filename="Graveyard.stl", + ) + + self.graveyard = graveyard_shape + + return graveyard_shape + + def export_h5m( + self, + filename='dagmc.h5m', + skip_graveyard=False, + tolerance=0.001, + graveyard_offset=100): + """Converts stl files into DAGMC compatible h5m file using PyMOAB. The + DAGMC file produced has not been imprinted and merged unlike the other + supported method which uses Trelis to produce an imprinted and merged + DAGMC geometry. If the provided filename doesn't end with .h5m it will + be added + + Args: + filename (str, optional): filename of h5m outputfile + Defaults to "dagmc.h5m". + skip_graveyard (boolean, optional): filename of h5m outputfile + Defaults to False. + tolerance (float, optional): the precision of the faceting + Defaults to 0.001. + graveyard_offset (float, optional): the offset between the largest + edge of the geometry and inner bounding shell created. Defualts + to 100. + Returns: + filename: output h5m filename + """ + + path_filename = Path(filename) + + if path_filename.suffix != ".h5m": + path_filename = path_filename.with_suffix(".h5m") + + path_filename.parents[0].mkdir(parents=True, exist_ok=True) + + self.export_stl(self.stl_filename, tolerance=tolerance) + + moab_core, moab_tags = define_moab_core_and_tags() + + moab_core = add_stl_to_moab_core( + moab_core, + 1, + 1, + self.material_tag, + moab_tags, + self.stl_filename + ) + + if skip_graveyard is False: + self.make_graveyard(graveyard_offset=graveyard_offset) + self.graveyard.export_stl(self.graveyard.stl_filename) + volume_id = 2 + surface_id = 2 + moab_core = add_stl_to_moab_core( + moab_core, + surface_id, + volume_id, + self.graveyard.material_tag, + moab_tags, + self.graveyard.stl_filename + ) + + all_sets = moab_core.get_entities_by_handle(0) + + file_set = moab_core.create_meshset() + + moab_core.add_entities(file_set, all_sets) + + moab_core.write_file(str(path_filename)) + + return filename + + def export_graveyard( + self, + graveyard_offset=100, + filename="Graveyard.stp"): + """Writes an stp file (CAD geometry) for the reactor graveyard. This + is needed for DAGMC simulations. This method also calls + Reactor.make_graveyard with the offset. + + Args: + filename (str): the filename for saving the stp file + graveyard_offset (float): the offset between the largest edge of + the geometry and inner bounding shell created. Defaults to + Reactor.graveyard_offset + + Returns: + str: the stp filename created + """ + + self.make_graveyard(graveyard_offset=graveyard_offset) + self.graveyard.export_stp(Path(filename)) + + return filename diff --git a/paramak/utils.py b/paramak/utils.py new file mode 100644 index 000000000..4ba232328 --- /dev/null +++ b/paramak/utils.py @@ -0,0 +1,362 @@ + +import math +from collections import Iterable +from hashlib import blake2b + +import cadquery as cq +import numpy as np + +import paramak + + +def coefficients_of_line_from_points(point_a, point_b): + """Computes the m and c coefficients of the equation (y=mx+c) for + a straight line from two points. + + Args: + point_a (float, float): point 1 coordinates + point_b (float, float): point 2 coordinates + + Returns: + (float, float): m coefficient and c coefficient + """ + + points = [point_a, point_b] + x_coords, y_coords = zip(*points) + coord_array = np.vstack([x_coords, np.ones(len(x_coords))]).T + m, c = np.linalg.lstsq(coord_array, y_coords, rcond=None)[0] + return m, c + + +def cut_solid(solid, cutter): + """ + Performs a boolean cut of a solid with another solid or iterable of solids. + + Args: + solid Shape: The Shape that you want to cut from + cutter Shape: The Shape(s) that you want to be the cutting object + + Returns: + Shape: The original shape cut with the cutter shape(s) + """ + + # Allows for multiple cuts to be applied + if isinstance(cutter, Iterable): + for cutting_solid in cutter: + solid = solid.cut(cutting_solid.solid) + else: + solid = solid.cut(cutter.solid) + return solid + + +def diff_between_angles(angle_a, angle_b): + """Calculates the difference between two angles angle_a and angle_b + + Args: + angle_a (float): angle in degree + angle_b (float): angle in degree + + Returns: + float: difference between the two angles in degree. + """ + + delta_mod = (angle_b - angle_a) % 360 + if delta_mod > 180: + delta_mod -= 360 + return delta_mod + + +def distance_between_two_points(point_a, point_b): + """Computes the distance between two points. + + Args: + point_a (float, float): X, Y coordinates of the first point + point_b (float, float): X, Y coordinates of the second point + + Returns: + float: distance between A and B + """ + + xa, ya = point_a + xb, yb = point_b + u_vec = [xb - xa, yb - ya] + return np.linalg.norm(u_vec) + + +def extend(point_a, point_b, L): + """Creates a point C in (ab) direction so that \\|aC\\| = L + + Args: + point_a (float, float): X, Y coordinates of the first point + point_b (float, float): X, Y coordinates of the second point + L (float): distance AC + Returns: + float, float: point C coordinates + """ + + xa, ya = point_a + xb, yb = point_b + u_vec = [xb - xa, yb - ya] + u_vec /= np.linalg.norm(u_vec) + + xc = xa + L * u_vec[0] + yc = ya + L * u_vec[1] + return xc, yc + + +def find_center_point_of_circle(point_a, point_b, point3): + """ + Calculates the center and the radius of a circle + passing through 3 points. + Args: + point_a (float, float): point 1 coordinates + point_b (float, float): point 2 coordinates + point3 (float, float): point 3 coordinates + Returns: + float, float: center of the circle coordinates or + None if 3 points on a line are input. + """ + + temp = point_b[0] * point_b[0] + point_b[1] * point_b[1] + bc = (point_a[0] * point_a[0] + point_a[1] * point_a[1] - temp) / 2 + cd = (temp - point3[0] * point3[0] - point3[1] * point3[1]) / 2 + det = (point_a[0] - point_b[0]) * (point_b[1] - point3[1]) - ( + point_b[0] - point3[0] + ) * (point_a[1] - point_b[1]) + + if abs(det) < 1.0e-6: + return (None, np.inf) + + # Center of circle + cx = (bc * (point_b[1] - point3[1]) - cd * (point_a[1] - point_b[1])) / det + cy = ((point_a[0] - point_b[0]) * cd - (point_b[0] - point3[0]) * bc) / det + + radius = np.sqrt((cx - point_a[0]) ** 2 + (cy - point_a[1]) ** 2) + + return (cx, cy), radius + + +def intersect_solid(solid, intersecter): + """ + Performs a boolean intersection of a solid with another solid or iterable of + solids. + Args: + solid Shape: The Shape that you want to intersect + intersecter Shape: The Shape(s) that you want to be the intersecting object + Returns: + Shape: The original shape cut with the intersecter shape(s) + """ + + # Allows for multiple cuts to be applied + if isinstance(intersecter, Iterable): + for intersecting_solid in intersecter: + solid = solid.intersect(intersecting_solid.solid) + else: + solid = solid.intersect(intersecter.solid) + return solid + + +def rotate(origin, point, angle): + """ + Rotate a point counterclockwise by a given angle around a given origin. + The angle should be given in radians. + + Args: + origin (float, float): coordinates of origin point + point (float, float): coordinates of point to be rotated + angle (float): rotation angle in radians (counterclockwise) + Returns: + float, float: rotated point coordinates. + """ + + ox, oy = origin + px, py = point + + qx = ox + math.cos(angle) * (px - ox) - math.sin(angle) * (py - oy) + qy = oy + math.sin(angle) * (px - ox) + math.cos(angle) * (py - oy) + return qx, qy + + +def union_solid(solid, joiner): + """ + Performs a boolean union of a solid with another solid or iterable of solids + + Args: + solid (Shape): The Shape that you want to union from + joiner (Shape): The Shape(s) that you want to form the union with the + solid + Returns: + Shape: The original shape union with the joiner shape(s) + """ + + # Allows for multiple unions to be applied + if isinstance(joiner, Iterable): + for joining_solid in joiner: + solid = solid.union(joining_solid.solid) + else: + solid = solid.union(joiner.solid) + return solid + + +def calculate_wedge_cut(self): + """Calculates a wedge cut with the given rotation_angle""" + + if self.rotation_angle == 360: + return None + + cutting_wedge = paramak.CuttingWedgeFS(self) + return cutting_wedge + + +def add_thickness(x, y, thickness, dy_dx=None): + """Computes outer curve points based on thickness + + Args: + x (list): list of floats containing x values + y (list): list of floats containing y values + thickness (float): thickness of the magnet + dy_dx (list): list of floats containing the first order + derivatives + + Returns: + (list, list): R and Z lists for outer curve points + """ + + if dy_dx is None: + dy_dx = np.diff(y) / np.diff(x) + + x_outer, y_outer = [], [] + for i in range(len(dy_dx)): + if dy_dx[i] == float('-inf'): + nx, ny = -1, 0 + elif dy_dx[i] == float('inf'): + nx, ny = 1, 0 + else: + nx = -dy_dx[i] + ny = 1 + if i != len(dy_dx) - 1: + if x[i] < x[i + 1]: + convex = False + else: + convex = True + + if convex: + nx *= -1 + ny *= -1 + # normalise normal vector + normal_vector_norm = (nx ** 2 + ny ** 2) ** 0.5 + nx /= normal_vector_norm + ny /= normal_vector_norm + # calculate outer points + val_x_outer = x[i] + thickness * nx + val_y_outer = y[i] + thickness * ny + x_outer.append(val_x_outer) + y_outer.append(val_y_outer) + + return x_outer, y_outer + + +def get_hash(shape, ignored_keys=[]): + """Computes a unique hash vaue for the shape. + + Args: + shape (list): The paramak.Shape object to find the hash value for. + ignored_keys (list, optional): list of shape.__dict__ keys to ignore + when creating the hash. + + Returns: + (list, list): R and Z lists for outer curve points + """ + + hash_object = blake2b() + shape_dict = dict(shape.__dict__) + + for key in ignored_keys: + if key in shape_dict.keys(): + shape_dict[key] = None + + hash_object.update(str(list(shape_dict.values())).encode("utf-8")) + value = hash_object.hexdigest() + return value + + +class FaceAreaSelector(cq.Selector): + """A custom CadQuery selector the selects faces based on their area with a + tolerance. The following useage example will fillet the faces of an extrude + shape with an area of 0.5. paramak.ExtrudeStraightShape(points=[(1,1),(2,1), + (2,2)], distance=5).solid.faces(FaceAreaSelector(0.5)).fillet(0.1) + + Args: + area (float): The area of the surface to select. + tolerance (float, optional): The allowable tolerance of the length + (+/-) while still being selected by the custom selector. + """ + + def __init__(self, area, tol=0.1): + self.area = area + self.tol = tol + + def filter(self, objectList): + """Loops through all the faces in the object checking if the face + meets the custom selector requirments or not. + + Args: + objectList (cadquery): The object to filter the faces from. + + Returns: + objectList (cadquery): The face that match the selector area within + the specified tolerance. + """ + + new_obj_list = [] + for obj in objectList: + face_area = obj.Area() + + # Only return faces that meet the requirements + if face_area > self.area - self.tol and face_area < self.area + self.tol: + new_obj_list.append(obj) + + return new_obj_list + + +class EdgeLengthSelector(cq.Selector): + """A custom CadQuery selector the selects edges based on their length with + a tolerance. The following useage example will fillet the inner edge of a + rotated triangular shape. paramak.RotateStraightShape(points=[(1,1),(2,1), + (2,2)]).solid.edges(paramak.EdgeLengthSelector(6.28)).fillet(0.1) + + Args: + length (float): The length of the edge to select. + tolerance (float, optional): The allowable tolerance of the length + (+/-) while still being selected by the custom selector. + + """ + + def __init__(self, length, tol=0.1): + self.length = length + self.tol = tol + + def filter(self, objectList): + """Loops through all the edges in the object checking if the edge + meets the custom selector requirments or not. + + Args: + objectList (cadquery): The object to filter the edges from. + + Returns: + objectList (cadquery): The edge that match the selector length + within the specified tolerance. + """ + + new_obj_list = [] + print('filleting edge#') + for obj in objectList: + + edge_len = obj.Length() + + # Only return edges that meet our requirements + if edge_len > self.length - self.tol and edge_len < self.length + self.tol: + + new_obj_list.append(obj) + print('length(new_obj_list)', len(new_obj_list)) + return new_obj_list diff --git a/readthedocs.yml b/readthedocs.yml new file mode 100644 index 000000000..9808f9f4f --- /dev/null +++ b/readthedocs.yml @@ -0,0 +1,23 @@ +# .readthedocs.yml +# Read the Docs configuration file + +# Required +version: 1 + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/source/conf.py + +# Optional additional build formats +# formats: +# - pdf + +# this is not needed if using conda environment +# python: +# version: 3.7 +# install: +# - requirements: requirements.txt + +# specify conda environment needed for build +conda: + file: environment.yml \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..9302ecce1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,17 @@ +scipy +sympy +plotly +pytest +pytest-cov +numpy +plasmaboundaries +matplotlib +pillow + +# Note, cadquery not installed via pip to avoid installing cadquery version 1 + +# cython # needed for export_h5m with pymoab + +# Additional packages needed for neutronics simulations +# neutronics_material_maker +# parametric_plasma_source diff --git a/run_tests.sh b/run_tests.sh new file mode 100644 index 000000000..e600350eb --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,11 @@ +pytest tests/test_neutronics_utils.py -v --cov=paramak --cov-append --cov-report term --cov-report xml +pytest tests/test_utils.py -v --cov=paramak --cov-append --cov-report term --cov-report xml +pytest tests/test_Shape.py -v --cov=paramak --cov-append --cov-report term --cov-report xml +pytest tests/test_Reactor.py -v --cov=paramak --cov-append --cov-report term --cov-report xml +pytest tests/test_parametric_shapes/ -v --cov=paramak --cov-append --cov-report term --cov-report xml +pytest tests/test_parametric_components/ -v --cov=paramak --cov-append --cov-report term --cov-report xml +pytest tests/test_parametric_reactors/ -v --cov=paramak --cov-append --cov-report term --cov-report xml +pytest tests/test_example_shapes.py -v --cov=paramak --cov-append --cov-report term --cov-report xml +pytest tests/test_example_components.py -v --cov=paramak --cov-append --cov-report term --cov-report xml +pytest tests/test_example_reactors.py -v --cov=paramak --cov-append --cov-report term --cov-report xml +pytest tests/test_parametric_neutronics/ -v --cov=paramak --cov-append --cov-report term --cov-report xml \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 000000000..70e531b66 --- /dev/null +++ b/setup.py @@ -0,0 +1,39 @@ +import setuptools + +with open("README.md", "r") as fh: + long_description = fh.read() + +setuptools.setup( + name="paramak", + version="0.1.0", + author="The Paramak Development Team", + author_email="jonathan.shimwell@ukaea.uk", + description="Create 3D fusion reactor CAD models based on input parameters", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/ukaea/paramak", + packages=setuptools.find_packages(), + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + tests_require=[ + "pytest-cov", + "pytest-runner", + ], + install_requires=[ + "pytest-cov", + "plotly", + "scipy", + "sympy", + "numpy", + "tqdm", + "matplotlib", + "plasmaboundaries", + ], + extras_require={ + "neutronics": [ + "neutronics_material_maker", + "parametric_plasma_source", + ]}) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/local_test_examples.py b/tests/local_test_examples.py new file mode 100644 index 000000000..9cf919873 --- /dev/null +++ b/tests/local_test_examples.py @@ -0,0 +1,92 @@ + +import json +import os +import unittest +from pathlib import Path + +import paramak +import pytest + +cwd = os.getcwd() + +"These tests require a visual front end which is not well suported on docker based CI systems" + + +class TestLocalExamples(unittest.TestCase): + def test_make_collarge(self): + """ Runs the example and checks the output files are produced""" + os.chdir(Path(cwd)) + os.chdir(Path("examples")) + output_filenames = [ + "output_collarge/1.png", + "output_collarge/2.png", + "output_collarge/3.png", + "output_collarge/4.png", + "output_collarge/5.png", + "output_collarge/6.png", + "output_collarge/7.png", + "output_collarge/8.png", + "output_collarge/9.png", + "output_collarge/combine_images1.sh", + "output_collarge/combine_images2.sh", + "output_collarge/paramak_array1.svg", + "output_collarge/paramak_array2.svg", + ] + for output_filename in output_filenames: + os.system("rm " + output_filename) + os.system("python make_collarge.py") + for output_filename in output_filenames: + assert Path(output_filename).exists() + os.system("rm " + output_filename) + + def test_make_paramak_animation(self): + """ Runs the example and checks the output files are produced""" + os.chdir(Path(cwd)) + os.chdir(Path("examples")) + output_filenames = [ + "output_for_animation_2d/0.png", + "output_for_animation_2d/1.png", + "output_for_animation_2d/2.png", + "output_for_animation_3d/0.png", + "output_for_animation_3d/1.png", + "output_for_animation_3d/2.png", + "2d.gif", + "3d.gif", + ] + for output_filename in output_filenames: + os.system("rm " + output_filename) + os.system("python make_animation.py -n 3") + for output_filename in output_filenames: + assert Path(output_filename).exists() + os.system("rm " + output_filename) + + def test_export_3d_image(self): + """checks that export_3d_image() exports png files with the correct suffix""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20), (20, 0)] + ) + test_shape.rotation_angle = 360 + os.system("rm filename.png") + test_shape.export_3d_image("filename") + assert Path("filename.png").exists() is True + os.system("rm filename.png") + test_shape.export_3d_image("filename.png") + assert Path("filename.png").exists() is True + os.system("rm filename.png") + + def test_neutronics_cell_tally(self): + """ Runs the neutronics example and checks the TBR""" + os.chdir(Path(cwd)) + os.chdir(Path("examples/neutronics")) + output_filename = "simulation_result.json" + os.system("rm " + output_filename) + os.system("python make_simple_neutronics_model.py") + with open(output_filename) as json_file: + data = json.load(json_file) + assert data["TBR"] == pytest.approx(0.456, abs=0.01) + os.system("rm " + output_filename) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_Reactor.py b/tests/test_Reactor.py new file mode 100644 index 000000000..2c99cce1f --- /dev/null +++ b/tests/test_Reactor.py @@ -0,0 +1,796 @@ + +import json +import os +import unittest +from pathlib import Path + +import cadquery as cq +import paramak +import pytest + + +class TestReactor(unittest.TestCase): + + def test_adding_shape_with_material_tag_to_reactor(self): + """Checks that a shape object can be added to a Reactor object with + the correct material tag property.""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], material_tag="mat1" + ) + test_shape.rotation_angle = 360 + test_shape.create_solid() + test_reactor = paramak.Reactor([test_shape]) + assert len(test_reactor.material_tags) == 1 + assert test_reactor.material_tags[0] == "mat1" + + def test_adding_multiple_shapes_with_material_tag_to_reactor(self): + """Checks that multiple shape objects can be added to a Reactor object + with the correct material tag properties.""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], material_tag="mat1" + ) + test_shape2 = paramak.RotateSplineShape( + points=[(0, 0), (0, 20), (20, 20)], material_tag="mat2" + ) + test_shape.rotation_angle = 360 + test_shape.create_solid() + test_reactor = paramak.Reactor([test_shape, test_shape2]) + assert len(test_reactor.material_tags) == 2 + assert "mat1" in test_reactor.material_tags + assert "mat2" in test_reactor.material_tags + + def test_adding_shape_with_stp_filename_to_reactor(self): + """Checks that a shape object can be added to a Reactor object with the + correct stp filename property.""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], stp_filename="filename.stp" + ) + test_shape.rotation_angle = 360 + test_shape.create_solid() + test_reactor = paramak.Reactor([test_shape]) + assert len(test_reactor.stp_filenames) == 1 + assert test_reactor.stp_filenames[0] == "filename.stp" + + def test_adding_multiple_shape_with_stp_filename_to_reactor(self): + """Checks that multiple shape objects can be added to a Reactor object + with the correct stp filename properties.""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], stp_filename="filename.stp" + ) + test_shape2 = paramak.RotateSplineShape( + points=[(0, 0), (0, 20), (20, 20)], stp_filename="filename2.stp" + ) + test_shape.rotation_angle = 360 + test_shape.create_solid() + test_reactor = paramak.Reactor([test_shape, test_shape2]) + assert len(test_reactor.stp_filenames) == 2 + assert test_reactor.stp_filenames[0] == "filename.stp" + assert test_reactor.stp_filenames[1] == "filename2.stp" + + def test_adding_shape_with_duplicate_stp_filename_to_reactor(self): + """Adds shapes to a Reactor object to check errors are raised + correctly.""" + + def test_stp_filename_duplication(): + """Checks ValueError is raised when shapes with the same stp + filenames are added to a reactor object""" + + test_shape_1 = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], stp_filename="filename.stp" + ) + test_shape_2 = paramak.RotateSplineShape( + points=[(0, 0), (0, 20), (20, 20)], stp_filename="filename.stp" + ) + test_shape_1.rotation_angle = 90 + my_reactor = paramak.Reactor([test_shape_1, test_shape_2]) + my_reactor.export_stp() + + self.assertRaises(ValueError, test_stp_filename_duplication) + + def test_adding_shape_with_duplicate_stl_filename_to_reactor(self): + """Adds shapes to a Reactor object to checks errors are raised + correctly""" + + def test_stl_filename_duplication(): + """Checks ValueError is raised when shapes with the same stl + filenames are added to a reactor object""" + + test_shape_1 = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], stl_filename="filename.stl" + ) + test_shape_2 = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], stl_filename="filename.stl" + ) + test_shape_1.rotation_angle = 90 + my_reactor = paramak.Reactor([test_shape_1, test_shape_2]) + my_reactor.export_stl() + + sefl.assertRaises(ValueError, test_stl_filename_duplication) + + def test_adding_shape_with_None_stp_filename_to_reactor(self): + """adds shapes to a Reactor object to check errors are raised correctly""" + + def test_stp_filename_None(): + """checks ValueError is raised when RotateStraightShapes with duplicate + stp filenames are added""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], stp_filename="filename.stp" + ) + test_shape2 = paramak.RotateSplineShape( + points=[(0, 0), (0, 20), (20, 20)], stp_filename=None + ) + test_shape.create_solid() + my_reactor = paramak.Reactor([test_shape, test_shape2]) + my_reactor.export_stp() + + self.assertRaises(ValueError, test_stp_filename_None) + + def test_adding_shape_with_duplicate_stl_filename_to_reactor(self): + """Adds shapes to a Reactor object to check errors are raised + correctly.""" + + def test_stl_filename_duplication_rotate_straight(): + """checks ValueError is raised when RotateStraightShapes with + duplicate stl filenames are added""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], stl_filename="filename.stl" + ) + test_shape2 = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], stl_filename="filename.stl" + ) + test_shape.rotation_angle = 360 + test_shape.create_solid() + my_reactor = paramak.Reactor([test_shape, test_shape2]) + my_reactor.export_stl() + + self.assertRaises( + ValueError, + test_stl_filename_duplication_rotate_straight) + + def test_stl_filename_duplication_rotate_spline(): + """Checks ValueError is raised when RotateSplineShapes with + duplicate stl filenames are added.""" + + test_shape = paramak.RotateSplineShape( + points=[(0, 0), (0, 20), (20, 20)], stl_filename="filename.stl" + ) + test_shape2 = paramak.RotateSplineShape( + points=[(0, 0), (0, 20), (20, 20)], stl_filename="filename.stl" + ) + test_shape.rotation_angle = 360 + test_shape.create_solid() + my_reactor = paramak.Reactor([test_shape, test_shape2]) + my_reactor.export_stl() + + self.assertRaises( + ValueError, + test_stl_filename_duplication_rotate_spline) + + def test_stl_filename_duplication_rotate_mixed(): + """Checks ValueError is raised when RotateMixedShapes with + duplicate stl filenames are added.""" + + test_shape = paramak.RotateMixedShape( + points=[(0, 0, "straight"), (0, 20, "straight"), (20, 20, "straight")], + stl_filename="filename.stl", + ) + test_shape2 = paramak.RotateMixedShape( + points=[(0, 0, "straight"), (0, 20, "straight"), (20, 20, "straight")], + stl_filename="filename.stl", + ) + test_shape.rotation_angle = 360 + test_shape.create_solid() + my_reactor = paramak.Reactor([test_shape, test_shape2]) + my_reactor.export_stl() + + self.assertRaises( + ValueError, + test_stl_filename_duplication_rotate_mixed) + + def test_stl_filename_duplication_Rotate_Circle(): + """Checks ValueError is raised when RotateCircleShapes with + duplicate stl filenames are added.""" + + test_shape = paramak.RotateCircleShape( + points=[(20, 20)], + radius=10, + rotation_angle=180, + stl_filename="filename.stl", + ) + test_shape2 = paramak.RotateCircleShape( + points=[(20, 20)], + radius=10, + rotation_angle=180, + stl_filename="filename.stl", + ) + test_shape.rotation_angle = 360 + test_shape.create_solid() + my_reactor = paramak.Reactor([test_shape, test_shape2]) + my_reactor.export_stl() + + self.assertRaises( + ValueError, + test_stl_filename_duplication_Rotate_Circle) + + def test_stl_filename_duplication_Extrude_straight(): + """Checks ValueError is raised when ExtrudeStraightShapes with + duplicate stl filenames are added.""" + + test_shape = paramak.ExtrudeStraightShape( + points=[(0, 0), (0, 20), (20, 20)], + distance=10, + stl_filename="filename.stl", + ) + test_shape2 = paramak.ExtrudeStraightShape( + points=[(0, 0), (0, 20), (20, 20)], + distance=10, + stl_filename="filename.stl", + ) + test_shape.rotation_angle = 360 + test_shape.create_solid() + my_reactor = paramak.Reactor([test_shape, test_shape2]) + my_reactor.export_stl() + + self.assertRaises( + ValueError, + test_stl_filename_duplication_Extrude_straight) + + def test_stl_filename_duplication_Extrude_spline(): + """Checks ValueError is raised when ExtrudeSplineShapes with + duplicate stl filenames are added.""" + + test_shape = paramak.ExtrudeSplineShape( + points=[(0, 0), (0, 20), (20, 20)], + distance=10, + stl_filename="filename.stl", + ) + test_shape2 = paramak.ExtrudeSplineShape( + points=[(0, 0), (0, 20), (20, 20)], + distance=10, + stl_filename="filename.stl", + ) + test_shape.rotation_angle = 360 + test_shape.create_solid() + my_reactor = paramak.Reactor([test_shape, test_shape2]) + my_reactor.export_stl() + + self.assertRaises( + ValueError, + test_stl_filename_duplication_Extrude_spline) + + def test_stl_filename_duplication_Extrude_mixed(): + """checks ValueError is raised when ExtrudeMixedShapes with duplicate + stl filenames are added""" + + test_shape = paramak.ExtrudeMixedShape( + points=[(0, 0, "straight"), (0, 20, "straight"), (20, 20, "straight")], + distance=10, + stl_filename="filename.stl", + ) + test_shape2 = paramak.ExtrudeMixedShape( + points=[(0, 0, "straight"), (0, 20, "straight"), (20, 20, "straight")], + distance=10, + stl_filename="filename.stl", + ) + test_shape.rotation_angle = 360 + test_shape.create_solid() + my_reactor = paramak.Reactor([test_shape, test_shape2]) + my_reactor.export_stl() + + self.assertRaises( + ValueError, + test_stl_filename_duplication_Extrude_mixed) + + def test_stl_filename_duplication_Extrude_Circle(): + """checks ValueError is raised when ExtrudeCircleShapes with duplicate + stl filenames are added""" + + test_shape = paramak.ExtrudeCircleShape( + points=[(20, 20)], radius=10, distance=10, stl_filename="filename.stl" + ) + test_shape2 = paramak.ExtrudeCircleShape( + points=[(20, 20)], radius=10, distance=10, stl_filename="filename.stl" + ) + test_shape.rotation_angle = 360 + test_shape.create_solid() + my_reactor = paramak.Reactor([test_shape, test_shape2]) + my_reactor.export_stl() + + self.assertRaises( + ValueError, + test_stl_filename_duplication_Extrude_Circle) + + def test_stl_filename_None(): + test_shape = paramak.ExtrudeCircleShape( + points=[(20, 20)], radius=10, distance=10, stl_filename=None + ) + my_reactor = paramak.Reactor([test_shape]) + my_reactor.export_stl() + + self.assertRaises( + ValueError, + test_stl_filename_None) + + def test_reactor_creation_with_default_properties(self): + """creates a Reactor object and checks that it has no default properties""" + + test_reactor = paramak.Reactor([]) + + assert test_reactor is not None + + def test_adding_component_to_reactor(self): + """creates a Reactor object and checks that shapes can be added to it""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)]) + test_shape.rotation_angle = 360 + test_shape.create_solid() + test_reactor = paramak.Reactor([]) + assert len(test_reactor.shapes_and_components) == 0 + test_reactor = paramak.Reactor([test_shape]) + assert len(test_reactor.shapes_and_components) == 1 + + def test_Graveyard_exists(self): + """creates a Reactor object with one shape and checks that a graveyard + can be produced using the make_graveyard method""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)]) + test_shape.rotation_angle = 360 + test_shape.create_solid() + test_reactor = paramak.Reactor([test_shape]) + test_reactor.make_graveyard() + + assert isinstance(test_reactor.graveyard, paramak.Shape) + + def test_Graveyard_exists_solid_is_None(self): + """creates a Reactor object with one shape and checks that a graveyard + can be produced using the make_graveyard method when the solid + attribute of the shape is None""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)]) + test_shape.rotation_angle = 360 + test_shape.create_solid() + test_reactor = paramak.Reactor([test_shape]) + test_reactor.shapes_and_components[0].solid = None + test_reactor.make_graveyard() + + assert isinstance(test_reactor.graveyard, paramak.Shape) + + def test_export_graveyard(self): + """creates a Reactor object with one shape and checks that a graveyard + can be exported to a specified location using the make_graveyard method""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)]) + test_shape.rotation_angle = 360 + os.system("rm my_graveyard.stp") + os.system("rm Graveyard.stp") + test_shape.stp_filename = "test_shape.stp" + test_reactor = paramak.Reactor([test_shape]) + + test_reactor.export_graveyard() + test_reactor.export_graveyard(filename="my_graveyard.stp") + + for filepath in ["Graveyard.stp", "my_graveyard.stp"]: + assert Path(filepath).exists() is True + os.system("rm " + filepath) + + assert test_reactor.graveyard is not None + assert test_reactor.graveyard.__class__.__name__ == "HollowCube" + + def test_export_graveyard_offset(self): + """checks that the graveyard can be exported with the correct default parameters + and that these parameters can be changed""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)]) + os.system("rm Graveyard.stp") + test_reactor = paramak.Reactor([test_shape]) + test_reactor.export_graveyard() + assert test_reactor.graveyard_offset == 100 + graveyard_volume_1 = test_reactor.graveyard.volume + + test_reactor.export_graveyard(graveyard_offset=50) + assert test_reactor.graveyard.volume < graveyard_volume_1 + graveyard_volume_2 = test_reactor.graveyard.volume + + test_reactor.export_graveyard(graveyard_offset=200) + assert test_reactor.graveyard.volume > graveyard_volume_1 + assert test_reactor.graveyard.volume > graveyard_volume_2 + + def test_exported_stp_files_exist(self): + """creates a Reactor object with one shape and checks that a stp file + of the reactor can be exported to a specified location using the export_stp + method""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)]) + test_shape.rotation_angle = 360 + os.system("rm test_reactor/test_shape.stp") + os.system("rm test_reactor/Graveyard.stp") + test_shape.stp_filename = "test_shape.stp" + test_reactor = paramak.Reactor([test_shape]) + + test_reactor.export_stp(output_folder="test_reactor") + + for filepath in [ + "test_reactor/test_shape.stp", + "test_reactor/Graveyard.stp"]: + assert Path(filepath).exists() is True + os.system("rm " + filepath) + + def test_exported_stl_files_exist(self): + """creates a Reactor object with one shape and checks that a stl file + of the reactor can be exported to a specified location using the + export_stl method""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)]) + test_shape.rotation_angle = 360 + os.system("rm test_reactor/test_shape.stl") + os.system("rm test_reactor/Graveyard.stl") + test_shape.stl_filename = "test_shape.stl" + test_reactor = paramak.Reactor([test_shape]) + + test_reactor.export_stl(output_folder="test_reactor") + + for filepath in [ + "test_reactor/test_shape.stl", + "test_reactor/Graveyard.stl"]: + assert Path(filepath).exists() is True + os.system("rm " + filepath) + + def test_exported_svg_files_exist(self): + """Creates a Reactor object with one shape and checks that a svg file + of the reactor can be exported to a specified location using the + export_svg method.""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)]) + test_shape.rotation_angle = 360 + os.system("rm test_svg_image.svg") + test_reactor = paramak.Reactor([test_shape]) + + test_reactor.export_svg("test_svg_image.svg") + + assert Path("test_svg_image.svg").exists() is True + os.system("rm test_svg_image.svg") + + def test_exported_svg_files_exist_no_extension(self): + """creates a Reactor object with one shape and checks that an svg file + of the reactor can be exported to a specified location using the export_svg + method""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)]) + test_shape.rotation_angle = 360 + os.system("rm test_svg_image.svg") + test_reactor = paramak.Reactor([test_shape]) + + test_reactor.export_svg("test_svg_image") + + assert Path("test_svg_image.svg").exists() is True + os.system("rm test_svg_image.svg") + + def test_neutronics_description(self): + """Creates reactor objects to check errors are raised correctly when + exporting the neutronics description.""" + + def test_neutronics_description_without_material_tag(): + """Checks ValueError is raised when the neutronics description is + exported without material_tag.""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)]) + test_shape.rotation_angle = 360 + test_shape.stp_filename = "test.stp" + test_reactor = paramak.Reactor([test_shape]) + test_reactor.neutronics_description() + + self.assertRaises( + ValueError, + test_neutronics_description_without_material_tag) + + def test_neutronics_description_without_stp_filename(): + """Checks ValueError is raised when the neutronics description is + exported without stp_filename.""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)]) + test_shape.rotation_angle = 360 + test_shape.material_tag = "test_material" + test_shape.stp_filename = None + test_reactor = paramak.Reactor([test_shape]) + test_reactor.neutronics_description() + + self.assertRaises( + ValueError, + test_neutronics_description_without_stp_filename) + + def test_neutronics_description_without_plasma(self): + """Creates a Reactor object and checks that the neutronics description + is exported with the correct material_tag and stp_filename.""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)]) + test_shape.rotation_angle = 360 + test_shape.material_tag = "test_material" + test_shape.stp_filename = "test.stp" + test_reactor = paramak.Reactor([test_shape]) + neutronics_description = test_reactor.neutronics_description() + + assert len(neutronics_description) == 2 + assert "stp_filename" in neutronics_description[0].keys() + assert "material" in neutronics_description[0].keys() + assert neutronics_description[0]["material"] == "test_material" + assert neutronics_description[0]["stp_filename"] == "test.stp" + assert neutronics_description[1]["material"] == "Graveyard" + assert neutronics_description[1]["stp_filename"] == "Graveyard.stp" + + def test_export_neutronics_description(self): + """Creates a Reactor object and checks that the neutronics description + is exported to a json file with the correct material_name and + stp_filename.""" + + os.system("rm manifest_test.json") + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)]) + test_shape.rotation_angle = 360 + test_shape.material_tag = "test_material" + test_shape.stp_filename = "test.stp" + test_shape.tet_mesh = "size 60" + test_reactor = paramak.Reactor([test_shape]) + returned_filename = test_reactor.export_neutronics_description( + filename="manifest_test.json" + ) + with open("manifest_test.json") as json_file: + neutronics_description = json.load(json_file) + + assert returned_filename == "manifest_test.json" + assert Path("manifest_test.json").exists() is True + assert len(neutronics_description) == 2 + assert "stp_filename" in neutronics_description[0].keys() + assert "material" in neutronics_description[0].keys() + assert "tet_mesh" in neutronics_description[0].keys() + assert neutronics_description[0]["material"] == "test_material" + assert neutronics_description[0]["stp_filename"] == "test.stp" + assert neutronics_description[0]["tet_mesh"] == "size 60" + assert neutronics_description[1]["material"] == "Graveyard" + assert neutronics_description[1]["stp_filename"] == "Graveyard.stp" + os.system("rm manifest_test.json") + + def test_export_neutronics_description_with_plasma(self): + """Creates a Reactor object and checks that the neutronics description + is exported to a json file with the correct entries, including the + optional plasma.""" + + os.system("rm manifest_test.json") + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], + rotation_angle=360, + material_tag="test_material", + stp_filename="test.stp", + ) + test_shape.tet_mesh = "size 60" + test_plasma = paramak.Plasma( + major_radius=500, + minor_radius=100, + stp_filename="plasma.stp", + material_tag="DT_plasma", + ) + test_reactor = paramak.Reactor([test_shape, test_plasma]) + returned_filename = test_reactor.export_neutronics_description( + include_plasma=True + ) + with open("manifest.json") as json_file: + neutronics_description = json.load(json_file) + + assert returned_filename == "manifest.json" + assert Path("manifest.json").exists() is True + assert len(neutronics_description) == 3 + assert "stp_filename" in neutronics_description[0].keys() + assert "material" in neutronics_description[0].keys() + assert "tet_mesh" in neutronics_description[0].keys() + assert "stp_filename" in neutronics_description[1].keys() + assert "material" in neutronics_description[1].keys() + assert "tet_mesh" not in neutronics_description[1].keys() + assert neutronics_description[0]["material"] == "test_material" + assert neutronics_description[0]["stp_filename"] == "test.stp" + assert neutronics_description[0]["tet_mesh"] == "size 60" + assert neutronics_description[1]["material"] == "DT_plasma" + assert neutronics_description[1]["stp_filename"] == "plasma.stp" + assert neutronics_description[2]["material"] == "Graveyard" + assert neutronics_description[2]["stp_filename"] == "Graveyard.stp" + os.system("rm manifest.json") + + def test_export_neutronics_description_without_plasma(self): + """Creates a Reactor object and checks that the neutronics description is + exported to a json file with the correct entires, exluding the optional + plasma.""" + + os.system("rm manifest_test.json") + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], + rotation_angle=360, + material_tag="test_material", + stp_filename="test.stp", + ) + test_shape.tet_mesh = "size 60" + test_plasma = paramak.Plasma(major_radius=500, minor_radius=100) + test_reactor = paramak.Reactor([test_shape, test_plasma]) + returned_filename = test_reactor.export_neutronics_description() + with open("manifest.json") as json_file: + neutronics_description = json.load(json_file) + + assert returned_filename == "manifest.json" + assert Path("manifest.json").exists() is True + assert len(neutronics_description) == 2 + assert "stp_filename" in neutronics_description[0].keys() + assert "material" in neutronics_description[0].keys() + assert "tet_mesh" in neutronics_description[0].keys() + assert neutronics_description[0]["material"] == "test_material" + assert neutronics_description[0]["stp_filename"] == "test.stp" + assert neutronics_description[0]["tet_mesh"] == "size 60" + assert neutronics_description[1]["material"] == "Graveyard" + assert neutronics_description[1]["stp_filename"] == "Graveyard.stp" + os.system("rm manifest.json") + + def test_export_neutronics_without_extension(self): + """checks a json file is created if filename has no extension""" + + os.system("rm manifest_test.json") + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)]) + test_shape.rotation_angle = 360 + test_shape.material_tag = "test_material" + test_shape.stp_filename = "test.stp" + test_shape.tet_mesh = "size 60" + test_reactor = paramak.Reactor([test_shape]) + returned_filename = test_reactor.export_neutronics_description( + filename="manifest_test" + ) + assert returned_filename == "manifest_test.json" + assert Path("manifest_test.json").exists() is True + os.system("rm manifest_test.json") + + def test_export_2d_image(self): + """Creates a Reactor object and checks that a png file of the reactor + with the correct filename can be exported using the export_2D_image + method.""" + + os.system("rm 2D_test_image.png") + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)]) + test_shape.rotation_angle = 360 + test_reactor = paramak.Reactor([test_shape]) + returned_filename = test_reactor.export_2d_image( + filename="2D_test_image.png") + + assert Path(returned_filename).exists() is True + os.system("rm 2D_test_image.png") + + def test_export_2d_image_without_extension(self): + """creates a Reactor object and checks that a png file of the reactor + with the correct filename can be exported using the export_2d_image + method""" + + os.system("rm 2d_test_image.png") + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)]) + test_shape.rotation_angle = 360 + test_reactor = paramak.Reactor([test_shape]) + returned_filename = test_reactor.export_2d_image( + filename="2d_test_image") + + assert Path(returned_filename).exists() is True + os.system("rm 2d_test_image.png") + + def test_export_html(self): + """Creates a Reactor object and checks that a html file of the reactor + with the correct filename can be exported using the export_html + method.""" + + os.system("rm test_html.html") + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)]) + test_shape.rotation_angle = 360 + test_reactor = paramak.Reactor([test_shape]) + test_reactor.export_html(filename="test_html.html") + + assert Path("test_html.html").exists() is True + os.system("rm test_html.html") + test_reactor.export_html(filename="test_html") + + assert Path("test_html.html").exists() is True + os.system("rm test_html.html") + + def test_tet_meshes_error(self): + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)]) + test_shape.rotation_angle = 360 + test_reactor = paramak.Reactor([test_shape]) + assert test_reactor.tet_meshes is not None + + def test_largest_dimention(self): + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)]) + test_shape.rotation_angle = 360 + test_reactor = paramak.Reactor([test_shape]) + assert pytest.approx(test_reactor.largest_dimension, rel=0.1 == 20) + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (30, 20)]) + test_shape.rotation_angle = 360 + test_reactor = paramak.Reactor([test_shape]) + assert pytest.approx(test_reactor.largest_dimension, rel=0.1 == 30) + + def test_shapes_and_components(self): + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)]) + + def incorrect_shapes_and_components(): + paramak.Reactor(test_shape) + self.assertRaises(ValueError, incorrect_shapes_and_components) + + def test_graveyard_error(self): + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)]) + test_reactor = paramak.Reactor([test_shape]) + + def str_graveyard_offset(): + test_reactor.graveyard_offset = 'coucou' + + def negative_graveyard_offset(): + test_reactor.graveyard_offset = -2 + + def list_graveyard_offset(): + test_reactor.graveyard_offset = [1.2] + self.assertRaises(ValueError, str_graveyard_offset) + self.assertRaises(ValueError, negative_graveyard_offset) + self.assertRaises(ValueError, list_graveyard_offset) + + def test_compound_in_shapes(self): + shape1 = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)]) + shape2 = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)]) + shape3 = paramak.Shape() + shape3.solid = cq.Compound.makeCompound( + [a.val() for a in [shape1.solid, shape2.solid]] + ) + test_reactor = paramak.Reactor([shape3]) + assert test_reactor.solid is not None + + def test_adding_shape_with_None_stp_filename_physical_groups(self): + """adds shapes to a Reactor object to check errors are raised + correctly""" + + def test_stp_filename_None(): + """checks ValueError is raised when RotateStraightShapes with + None as stp filenames are added""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], stp_filename="filename.stp" + ) + test_shape2 = paramak.RotateSplineShape( + points=[(0, 0), (0, 20), (20, 20)], stp_filename=None + ) + test_shape.create_solid() + my_reactor = paramak.Reactor([test_shape, test_shape2]) + my_reactor.export_physical_groups() + + self.assertRaises(ValueError, test_stp_filename_None) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_Shape.py b/tests/test_Shape.py new file mode 100644 index 000000000..950f60a48 --- /dev/null +++ b/tests/test_Shape.py @@ -0,0 +1,561 @@ + +import os +import unittest +from pathlib import Path + +import paramak +import pytest + + +class TestShape(unittest.TestCase): + + def test_shape_default_properties(self): + """Creates a Shape object and checks that the points attribute has + a default of None.""" + + test_shape = paramak.Shape() + + assert test_shape.points is None + + def test_azimuth_placement_angle_getting_setting(self): + """Checks that the azimuth_placement_angle of a Shape can be + changed to a single value or iterable.""" + + test_shape = paramak.Shape() + + assert test_shape.azimuth_placement_angle == 0 + test_shape.azimuth_placement_angle = 180 + assert test_shape.azimuth_placement_angle == 180 + test_shape.azimuth_placement_angle = [0, 90, 180, 270] + assert test_shape.azimuth_placement_angle == [0, 90, 180, 270] + + def test_incorrect_color_values(self): + """Checks that an error is raised when the color of a shape is + defined as an invalid string.""" + + def incorrect_color_string(): + paramak.Shape(color=('1', '0', '1')) + + self.assertRaises( + ValueError, + incorrect_color_string + ) + + def test_incorrect_workplane(self): + """Creates Shape object with incorrect workplane and checks ValueError + is raised.""" + + test_shape = paramak.Shape() + + def incorrect_workplane(): + """Creates Shape object with unacceptable workplane.""" + + test_shape.workplane = "AB" + + self.assertRaises(ValueError, incorrect_workplane) + + def test_incorrect_points(self): + """Creates Shape objects and checks errors are raised correctly when + specifying points.""" + + test_shape = paramak.Shape() + + def incorrect_points_end_point_is_start_point(): + """Checks ValueError is raised when the start and end points are + the same.""" + + test_shape.points = [(0, 200), (200, 100), (0, 0), (0, 200)] + + self.assertRaises( + ValueError, + incorrect_points_end_point_is_start_point) + + def incorrect_points_missing_z_value(): + """Checks ValueError is raised when a point is missing a z + value.""" + + test_shape.points = [(0, 200), (200), (0, 0), (0, 50)] + + self.assertRaises(ValueError, incorrect_points_missing_z_value) + + def incorrect_points_not_a_list(): + """Checks ValueError is raised when the points are not a list.""" + + test_shape.points = (0, 0), (0, 20), (20, 20), (20, 0) + + self.assertRaises(ValueError, incorrect_points_not_a_list) + + def incorrect_points_wrong_number_of_entries(): + """Checks ValueError is raised when individual points dont have 2 + or 3 entries.""" + + test_shape.points = [(0, 0), (0, 20), (20, 20, 20, 20)] + + self.assertRaises(ValueError, incorrect_points_wrong_number_of_entries) + + def incorrect_x_point_value_type(): + """Checks ValueError is raised when X point is not a number.""" + + test_shape.points = [("string", 0), (0, 20), (20, 20)] + + self.assertRaises(ValueError, incorrect_x_point_value_type) + + def incorrect_y_point_value_type(): + """Checks ValueError is raised when Y point is not a number.""" + + test_shape.points = [(0, "string"), (0, 20), (20, 20)] + + self.assertRaises(ValueError, incorrect_y_point_value_type) + + def test_create_limits(self): + """Creates a Shape object and checks that the create_limits function + returns the expected values for x_min, x_max, z_min and z_max.""" + + test_shape = paramak.Shape() + + test_shape.points = [ + (0, 0), + (0, 10), + (0, 20), + (10, 20), + (20, 20), + (20, 10), + (20, 0), + (10, 0), + ] + + assert test_shape.create_limits() == (0.0, 20.0, 0.0, 20.0) + + # test with a component which has a find_points method + test_shape2 = paramak.Plasma() + test_shape2.create_limits() + assert test_shape2.x_min is not None + + def test_create_limits_error(self): + """Checks error is raised when no points are given.""" + + test_shape = paramak.Shape() + + def limits(): + test_shape.create_limits() + self.assertRaises(ValueError, limits) + + def test_export_2d_image(self): + """Creates a Shape object and checks that a png file of the object with + the correct suffix can be exported using the export_2d_image method.""" + + test_shape = paramak.Shape() + test_shape.points = [(0, 0), (0, 20), (20, 20), (20, 0)] + os.system("rm filename.png") + test_shape.export_2d_image("filename") + assert Path("filename.png").exists() is True + os.system("rm filename.png") + test_shape.export_2d_image("filename.png") + assert Path("filename.png").exists() is True + os.system("rm filename.png") + + def test_initial_solid_construction(self): + """Creates a shape and checks that a cadquery solid with a unique hash + value is created when .solid is called.""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20), (20, 0)], rotation_angle=360 + ) + + assert test_shape.hash_value is None + assert test_shape.solid is not None + assert type(test_shape.solid).__name__ == "Workplane" + assert test_shape.hash_value is not None + + def test_solid_return(self): + """Checks that the same cadquery solid with the same unique hash value + is returned when shape.solid is called again after no changes have been + made to the Shape.""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20), (20, 0)], rotation_angle=360 + ) + + assert test_shape.solid is not None + initial_hash_value = test_shape.hash_value + assert test_shape.solid is not None + assert initial_hash_value == test_shape.hash_value + + def test_conditional_solid_reconstruction(self): + """Checks that a new cadquery solid with a new unique hash value is + constructed when shape.solid is called after changes to the Shape have + been made.""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], rotation_angle=360 + ) + + assert test_shape.solid is not None + assert test_shape.hash_value is not None + initial_hash_value = test_shape.hash_value + + test_shape.rotation_angle = 180 + + assert test_shape.solid is not None + assert test_shape.hash_value is not None + assert initial_hash_value != test_shape.hash_value + + def test_hash_value_update(self): + """Checks that the hash value of a Shape is not updated until a new + cadquery solid has been created.""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], rotation_angle=360 + ) + test_shape.solid + assert test_shape.hash_value is not None + initial_hash_value = test_shape.hash_value + + test_shape.rotation_angle = 180 + assert test_shape.hash_value == initial_hash_value + test_shape.solid + assert test_shape.hash_value != initial_hash_value + + def test_material_tag_warning(self): + """Checks that a warning is raised when a Shape has a material tag > + 28 characters.""" + + test_shape = paramak.Shape() + + def warning_material_tag(): + + test_shape.material_tag = "abcdefghijklmnopqrstuvwxyz12345" + + self.assertWarns(UserWarning, warning_material_tag) + + def test_invalid_material_tag(self): + """Checks a ValueError is raised when a Shape has an invalid material + tag.""" + + test_shape = paramak.Shape() + + def invalid_material_tag(): + + test_shape.material_tag = 123 + + self.assertRaises(ValueError, invalid_material_tag) + + def test_export_html(self): + """Checks a plotly figure of the Shape is exported by the export_html + method with the correct filename with RGB and RGBA colors.""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20), (20, 0)], rotation_angle=360 + ) + + os.system("rm filename.html") + test_shape.export_html('filename') + assert Path("filename.html").exists() is True + os.system("rm filename.html") + test_shape.color = (1, 0, 0, 0.5) + test_shape.export_html('filename') + assert Path("filename.html").exists() is True + os.system("rm filename.html") + + def test_export_html_with_points_None(self): + """Checks that an error is raised when points is None and export_html + """ + test_shape = paramak.Shape() + + def export(): + test_shape.export_html("out.html") + self.assertRaises(ValueError, export) + + def test_invalid_stp_filename(self): + """Checks ValueError is raised when invalid stp filenames are used.""" + + def invalid_filename_suffix(): + + paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], + stp_filename="filename.invalid_suffix" + ) + + self.assertRaises(ValueError, invalid_filename_suffix) + + def invalid_filename_type(): + + paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], + stp_filename=123456 + ) + + self.assertRaises(ValueError, invalid_filename_type) + + def test_invalid_stl_filename(self): + """Checks ValueError is raised when invalid stl filenames are used.""" + + def invalid_filename_suffix(): + + paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], + stl_filename="filename.invalid_suffix" + ) + + self.assertRaises(ValueError, invalid_filename_suffix) + + def invalid_filename_type(): + + paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], + stl_filename=123456 + ) + + self.assertRaises(ValueError, invalid_filename_type) + + def test_invalid_color(self): + """Checks ValueError is raised when invalid colors are used.""" + + def invalid_color_type(): + + paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], + color=255 + ) + + self.assertRaises(ValueError, invalid_color_type) + + def invalid_color_length(): + + paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], + color=(255, 255, 255, 1, 1) + ) + + self.assertRaises(ValueError, invalid_color_length) + + def test_volumes_add_up_to_total_volume_Compound(self): + """Checks the volume and volumes attributes are correct types + and that the volumes sum to equalt the volume for a Compound.""" + + test_shape = paramak.PoloidalFieldCoilSet( + heights=[10, 10], + widths=[20, 20], + center_points=[(15, 15), (50, 50)] + ) + + assert isinstance(test_shape.volume, float) + assert isinstance(test_shape.volumes, list) + assert isinstance(test_shape.volumes[0], float) + assert isinstance(test_shape.volumes[1], float) + assert len(test_shape.volumes) == 2 + assert sum(test_shape.volumes) == pytest.approx(test_shape.volume) + + def test_volumes_add_up_to_total_volume(self): + """Checks the volume and volumes attributes are correct types + and that the volumes sum to equalt the volume.""" + + test_shape = paramak.PoloidalFieldCoil( + center_point=(100, 100), + height=50, + width=50 + ) + + assert isinstance(test_shape.volume, float) + assert isinstance(test_shape.volumes, list) + assert isinstance(test_shape.volumes[0], float) + assert len(test_shape.volumes) == 1 + assert sum(test_shape.volumes) == pytest.approx(test_shape.volume) + + def test_areas_add_up_to_total_area_Compound(self): + """Checks the area and areas attributes are correct types + and that the areas sum to equalt the area for a Compound.""" + + test_shape = paramak.PoloidalFieldCoilSet( + heights=[10, 10], + widths=[20, 20], + center_points=[(15, 15), (50, 50)] + ) + + assert isinstance(test_shape.area, float) + assert isinstance(test_shape.areas, list) + assert isinstance(test_shape.areas[0], float) + assert isinstance(test_shape.areas[1], float) + assert isinstance(test_shape.areas[2], float) + assert isinstance(test_shape.areas[3], float) + assert isinstance(test_shape.areas[4], float) + assert isinstance(test_shape.areas[5], float) + assert isinstance(test_shape.areas[6], float) + assert isinstance(test_shape.areas[7], float) + assert len(test_shape.areas) == 8 + assert sum(test_shape.areas) == pytest.approx(test_shape.area) + + def test_areas_add_up_to_total_area(self): + """Checks the area and areas attributes are correct types + and that the areas sum to equalt the area.""" + + test_shape = paramak.PoloidalFieldCoil( + center_point=(100, 100), + height=50, + width=50 + ) + + assert isinstance(test_shape.area, float) + assert isinstance(test_shape.areas, list) + assert isinstance(test_shape.areas[0], float) + assert isinstance(test_shape.areas[1], float) + assert isinstance(test_shape.areas[2], float) + assert isinstance(test_shape.areas[3], float) + assert len(test_shape.areas) == 4 + assert sum(test_shape.areas) == pytest.approx(test_shape.area) + + def test_trace(self): + """Test trace method is populated""" + + test_shape = paramak.PoloidalFieldCoil( + center_point=(100, 100), + height=50, + width=50, + name="coucou" + ) + assert test_shape._trace() is not None + + def test_create_patch_error(self): + """Checks _create_patch raises a ValueError when points is None.""" + + test_shape = paramak.Shape() + + def patch(): + test_shape._create_patch() + self.assertRaises(ValueError, patch) + + def test_create_patch_alpha(self): + """Checks _create_patch returns a patch when alpha is given.""" + + test_shape = paramak.PoloidalFieldCoil( + center_point=(100, 100), + height=50, + width=50, + color=(0.5, 0.5, 0.5, 0.1) + ) + assert test_shape._create_patch() is not None + + def test_azimuth_placement_angle_error(self): + """Checks an error is raised when invalid value for + azimuth_placement_angle is set. + """ + + test_shape = paramak.Shape() + + def angle_str(): + test_shape.azimuth_placement_angle = "coucou" + + def angle_str_in_Iterable(): + test_shape.azimuth_placement_angle = [0, "coucou"] + + self.assertRaises(ValueError, angle_str) + self.assertRaises(ValueError, angle_str_in_Iterable) + + def test_name_error(self): + """Checks an error is raised when invalid value for name is set.""" + + test_shape = paramak.Shape() + + def name_float(): + test_shape.name = 2.0 + + def name_int(): + test_shape.name = 1 + + def name_list(): + test_shape.name = ['coucou'] + + self.assertRaises(ValueError, name_float) + self.assertRaises(ValueError, name_int) + self.assertRaises(ValueError, name_list) + + def test_tet_mesh_error(self): + """Checks an error is raised when invalid value for tet_mesh is set. + """ + + test_shape = paramak.Shape() + + def tet_mesh_float(): + test_shape.tet_mesh = 2.0 + + def tet_mesh_int(): + test_shape.tet_mesh = 1 + + def tet_mesh_list(): + test_shape.tet_mesh = ['coucou'] + + self.assertRaises(ValueError, tet_mesh_float) + self.assertRaises(ValueError, tet_mesh_int) + self.assertRaises(ValueError, tet_mesh_list) + + def test_get_rotation_axis(self): + """Creates a shape and test the expected rotation_axis is the correct + values for several cases + """ + shape = paramak.Shape() + expected_dict = { + "X": [(-1, 0, 0), (1, 0, 0)], + "-X": [(1, 0, 0), (-1, 0, 0)], + "Y": [(0, -1, 0), (0, 1, 0)], + "-Y": [(0, 1, 0), (0, -1, 0)], + "Z": [(0, 0, -1), (0, 0, 1)], + "-Z": [(0, 0, 1), (0, 0, -1)], + } + # test with axis from string + for axis in expected_dict: + shape.rotation_axis = axis + assert shape.get_rotation_axis()[0] == expected_dict[axis] + assert shape.get_rotation_axis()[1] == axis + + # test with axis from list of two points + expected_axis = [(-1, -2, -3), (1, 4, 5)] + shape.rotation_axis = expected_axis + assert shape.get_rotation_axis()[0] == expected_axis + assert shape.get_rotation_axis()[1] == "custom_axis" + + # test with axis from workplane + shape.rotation_axis = None + + workplanes = ["XY", "XZ", "YZ"] + expected_axis = ["Y", "Z", "Z"] + for wp, axis in zip(workplanes, expected_axis): + shape.workplane = wp + assert shape.get_rotation_axis()[0] == expected_dict[axis] + assert shape.get_rotation_axis()[1] == axis + + # test with axis from path_workplane + for wp, axis in zip(workplanes, expected_axis): + shape.path_workplane = wp + assert shape.get_rotation_axis()[0] == expected_dict[axis] + assert shape.get_rotation_axis()[1] == axis + + def test_rotation_axis_error(self): + """Checks errors are raised when incorrect values of rotation_axis are + set + """ + incorrect_values = [ + "coucou", + 2, + 2.2, + [(1, 1, 1), 'coucou'], + [(1, 1, 1), 1], + [(1, 1, 1), 1.0], + [(1, 1, 1), (1, 1, 1)], + [(1, 1, 1), (1, 0, 1, 2)], + [(1, 1, 1, 2), (1, 0, 2)], + [(1, 1, 2), [1, 0, 2]], + [(1, 1, 1)], + [(1, 1, 1), (1, 'coucou', 1)], + [(1, 1, 1), (1, 0, 1), (1, 2, 3)], + ] + shape = paramak.Shape() + + def set_value(): + shape.rotation_axis = incorrect_values[i] + + for i in range(len(incorrect_values)): + self.assertRaises(ValueError, set_value) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_example_components.py b/tests/test_example_components.py new file mode 100644 index 000000000..4ceaa4ae2 --- /dev/null +++ b/tests/test_example_components.py @@ -0,0 +1,122 @@ + +import os +import sys +import unittest +from pathlib import Path + +from examples.example_parametric_components import ( + make_all_parametric_components, make_demo_style_blankets, + make_firstwall_for_neutron_wall_loading, make_plasmas, + make_vacuum_vessel_with_ports) + +sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'examples')) + + +sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'examples')) + + +class TestExampleComponents(unittest.TestCase): + + def test_make_all_parametric_components(self): + """Runs the example and checks the output files are produced""" + output_filenames = [ + 'plasma_shape.stp', + 'blanket_constant_thickness_outboard_plasma.stp', + 'blanket_constant_thickness_inboard_plasma.stp', + 'blanket_constant_thickness_plasma.stp', + 'center_column_shield_cylinder.stp', + 'firstwall_from_center_column_shield_cylinder.stp', + 'center_column_shield_hyperbola.stp', + 'firstwall_from_center_column_shield_hyperbola.stp', + 'center_column_shield_circular.stp', + 'firstwall_from_center_column_shield_circular.stp', + 'center_column_shield_flat_top_hyperbola.stp', + 'firstwall_from_center_column_shield_flat_top_hyperbola.stp', + 'center_column_shield_flat_top_Circular.stp', + 'firstwall_from_center_column_shield_flat_top_Circular.stp', + 'center_column_shield_plasma_hyperbola.stp', + 'firstwall_from_center_column_shield_plasma_hyperbola.stp', + 'inner_tf_coils_circular.stp', + 'inner_tf_coils_flat.stp', + 'pf_coil_case_set.stp', + 'pf_coil_set.stp', + 'pf_coil_cases_set.stp', + 'poloidal_field_coil.stp', + 'pf_coil_cases_set_fc.stp', + 'poloidal_field_coil_case_fc.stp', + 'poloidal_field_coil_case.stp', + 'blanket_arc_v.stp', + 'blanket_arc_h.stp', + 'tf_coil_rectangle.stp', + 'toroidal_field_coil_coat_hanger.stp', + 'toroidal_field_coil_triple_arc.stp', + 'toroidal_field_coil_princeton_d.stp', + 'ITER_type_divertor.stp'] + for output_filename in output_filenames: + os.system("rm " + output_filename) + all_components = make_all_parametric_components.main() + filenames = [] + for components in all_components: + components.export_stp() + filenames.append(components.stp_filename) + + for output_filename in output_filenames: + assert Path(output_filename).exists() is True + os.system("rm " + output_filename) + + def test_make_plasma(self): + """Runs the example and checks the output files are produced""" + output_filenames = [ + "ITER_plasma.html", + "EU_DEMO_plasma.html", + "ST_plasma.html", + "AST_plasma.html", + "ITER_plasma.stp", + "EU_DEMO_plasma.stp", + "ST_plasma.stp", + "AST_plasma.stp", + "ITER_plasma.png", + "EU_DEMO_plasma.png", + "ST_plasma.png", + "AST_plasma.png", + "all_plasma_and_points.html", + ] + for output_filename in output_filenames: + os.system("rm " + output_filename) + make_plasmas.main() + for output_filename in output_filenames: + assert Path(output_filename).exists() is True + os.system("rm " + output_filename) + + def test_make_demo_style_blanket(self): + """Runs the example and checks the output files are produced""" + output_filename = "blanket.stp" + os.system("rm " + output_filename) + make_demo_style_blankets.main() + assert Path(output_filename).exists() is True + os.system("rm " + output_filename) + + def test_make_segmented_firstwall(self): + """Runs the example and checks the output files are produced""" + output_filename = "segmented_firstwall.stp" + os.system("rm " + output_filename) + make_firstwall_for_neutron_wall_loading.main() + assert Path(output_filename).exists() is True + os.system("rm " + output_filename) + + def test_make_vacuum_vessel(self): + """Runs the example and checks the output files are produced""" + output_filenames = [ + "vacuum_vessel_with_ports.stp", + "vacuum_vessel_with_ports.svg", + ] + for output_filename in output_filenames: + os.system("rm " + output_filename) + make_vacuum_vessel_with_ports.main() + for output_filename in output_filenames: + assert Path(output_filename).exists() is True + os.system("rm " + output_filename) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_example_reactors.py b/tests/test_example_reactors.py new file mode 100644 index 000000000..5ee47f4a9 --- /dev/null +++ b/tests/test_example_reactors.py @@ -0,0 +1,130 @@ + +import os +import sys +import unittest +from pathlib import Path + +from examples.example_parametric_reactors import ( + ball_reactor, ball_reactor_single_null, submersion_reactor_single_null, + htc_reactor) + +sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'examples')) + + +class TestExampleReactors(unittest.TestCase): + def test_make_parametric_htc_rector(self): + """Runs the example to check the output files are produced""" + output_filenames = [ + "plasma.stp", + "inboard_pf_coils.stp", + "outboard_pf_coils.stp", + "div_coils.stp", + "vs_coils.stp", + "EFCCu_coils_1.stp", + "EFCCu_coils_2.stp", + "EFCCu_coils_3.stp", + "EFCCu_coils_4.stp", + "EFCCu_coils_5.stp", + "EFCCu_coils_6.stp", + "antenna.stp", + "tf_coil.stp", + "vacvessel.stp", + "inner_vessel.stp", + "Graveyard.stp", + ] + for output_filename in output_filenames: + os.system("rm " + output_filename) + htc_reactor.main(90) + for output_filename in output_filenames: + assert Path(output_filename).exists() is True + os.system("rm " + output_filename) + + def test_make_parametric_ball_rector(self): + """Runs the example and checks the output files are produced""" + output_filenames = [ + "plasma.stp", + "inboard_tf_coils.stp", + "center_column_shield.stp", + "divertor.stp", + "firstwall.stp", + "blanket.stp", + "blanket_rear_wall.stp", + "Graveyard.stp", + ] + for output_filename in output_filenames: + os.system("rm " + output_filename) + ball_reactor.make_ball_reactor(output_folder='') + for output_filename in output_filenames: + assert Path(output_filename).exists() is True + os.system("rm " + output_filename) + + def test_make_parametric_single_null_ball_reactor(self): + """Runs the example and checks the output files are produced""" + output_filenames = [ + "blanket_rear_wall.stp", + "blanket.stp", + "center_column_shield.stp", + "divertor.stp", + "firstwall.stp", + "Graveyard.stp", + "inboard_tf_coils.stp", + "pf_coils.stp", + "plasma.stp", + "tf_coil.stp" + ] + for output_filename in output_filenames: + os.system("rm " + output_filename) + ball_reactor_single_null.make_ball_reactor_sn(output_folder='') + for output_filename in output_filenames: + assert Path(output_filename).exists() is True + os.system("rm " + output_filename) + + def test_make_parametric_single_null_submersion_reactor(self): + """Runs the example and checks the output files are produced""" + output_filenames = [ + 'inboard_tf_coils.stp', + 'center_column_shield.stp', + 'plasma.stp', + 'divertor.stp', + 'supports.stp', + 'outboard_firstwall.stp', + 'blanket.stp', + 'outboard_rear_blanket_wall.stp', + 'outboard_tf_coil.stp', + 'pf_coils.stp', + 'Graveyard.stp' + ] + for output_filename in output_filenames: + os.system("rm " + output_filename) + submersion_reactor_single_null.make_submersion_sn(output_folder='') + for output_filename in output_filenames: + assert Path(output_filename).exists() is True + os.system("rm " + output_filename) + + def test_make_htc_reactor(self): + output_filenames = [ + 'inboard_pf_coils.stp', + 'outboard_pf_coils.stp', + 'div_coils.stp', + 'vs_coils.stp', + 'EFCCu_coils_1.stp', + 'EFCCu_coils_2.stp', + 'EFCCu_coils_3.stp', + 'EFCCu_coils_4.stp', + 'EFCCu_coils_5.stp', + 'EFCCu_coils_6.stp', + 'antenna.stp', + 'vacvessel.stp', + 'inner_vessel.stp', + 'htc_reactor.svg', + ] + for output_filename in output_filenames: + os.system("rm " + output_filename) + htc_reactor.main() + for output_filename in output_filenames: + assert Path(output_filename).exists() is True + os.system("rm " + output_filename) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_example_shapes.py b/tests/test_example_shapes.py new file mode 100644 index 000000000..8a47b6216 --- /dev/null +++ b/tests/test_example_shapes.py @@ -0,0 +1,90 @@ + +import os +import sys +import unittest +from pathlib import Path + +from examples.example_parametric_shapes import ( + make_blanket_from_parameters, make_blanket_from_points, + make_CAD_from_points, make_can_reactor_from_parameters, + make_can_reactor_from_points) + +sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'examples')) + + +class TestExampleShapes(unittest.TestCase): + + def test_make_blanket_from_points(self): + """Runs the example and checks the output files are produced""" + filename = "blanket_from_points.stp" + os.system("rm " + filename) + make_blanket_from_points.main(filename=filename) + assert Path(filename).exists() is True + os.system("rm " + filename) + + def test_make_blanket_parametrically(self): + """Runs the example and checks the output files are produced""" + filename = "blanket_from_parameters.stp" + os.system("rm " + filename) + make_blanket_from_parameters.main(filename=filename) + assert Path(filename).exists() is True + os.system("rm " + filename) + + def test_make_cad_from_points(self): + """Runs the example and checks the output files are produced""" + output_filenames = [ + "extruded_mixed.stp", + "extruded_straight.stp", + "extruded_spline.stp", + "rotated_mixed.stp", + "rotated_spline.stp", + "rotated_straights.stp", + ] + for output_filename in output_filenames: + os.system("rm " + output_filename) + make_CAD_from_points.main() + for output_filename in output_filenames: + assert Path(output_filename).exists() is True + os.system("rm " + output_filename) + + def test_make_can_reactor_from_parameters(self): + """Runs the example and checks the output files are produced""" + output_filenames = [ + "can_reactor_from_parameters/plasma.stp", + "can_reactor_from_parameters/centre_column.stp", + "can_reactor_from_parameters/blanket.stp", + "can_reactor_from_parameters/firstwall.stp", + "can_reactor_from_parameters/divertor_bottom.stp", + "can_reactor_from_parameters/divertor_top.stp", + "can_reactor_from_parameters/core.stp", + "can_reactor_from_parameters/reactor.html", + ] + for output_filename in output_filenames: + os.system("rm " + output_filename) + make_can_reactor_from_parameters.main() + for output_filename in output_filenames: + assert Path(output_filename).exists() is True + os.system("rm " + output_filename) + + def test_make_can_reactor_from_points(self): + """Runs the example and checks the output files are produced""" + output_filenames = [ + "can_reactor_from_points/plasma.stp", + "can_reactor_from_points/centre_column.stp", + "can_reactor_from_points/blanket.stp", + "can_reactor_from_points/firstwall.stp", + "can_reactor_from_points/divertor_bottom.stp", + "can_reactor_from_points/divertor_top.stp", + "can_reactor_from_points/core.stp", + "can_reactor_from_points/reactor.html", + ] + for output_filename in output_filenames: + os.system("rm " + output_filename) + make_can_reactor_from_points.main() + for output_filename in output_filenames: + assert Path(output_filename).exists() is True + os.system("rm " + output_filename) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_neutronics_utils.py b/tests/test_neutronics_utils.py new file mode 100644 index 000000000..5d3055c5d --- /dev/null +++ b/tests/test_neutronics_utils.py @@ -0,0 +1,42 @@ + +import unittest +from pathlib import Path + +import paramak +from paramak.neutronics_utils import (add_stl_to_moab_core, + define_moab_core_and_tags) + + +class TestNeutronicsUtilityFunctions(unittest.TestCase): + + def test_moab_instance_creation(self): + """passes three points on a circle to the function and checks that the + radius and center of the circle is calculated correctly""" + + moab_core, moab_tags = define_moab_core_and_tags() + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20), (20, 0)] + ) + + test_shape.export_stl('test_file.stl') + + new_moab_core = add_stl_to_moab_core( + moab_core=moab_core, + surface_id=1, + volume_id=1, + material_name='test_mat', + tags=moab_tags, + stl_filename='test_file.stl' + ) + + all_sets = new_moab_core.get_entities_by_handle(0) + + file_set = new_moab_core.create_meshset() + + new_moab_core.add_entities(file_set, all_sets) + + new_moab_core.write_file('test_file.h5m') + + assert Path('test_file.stl').exists() is True + assert Path('test_file.h5m').exists() is True diff --git a/tests/test_parametric_components/__init__.py b/tests/test_parametric_components/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_parametric_components/test_BlanketConstantThicknessArcH.py b/tests/test_parametric_components/test_BlanketConstantThicknessArcH.py new file mode 100644 index 000000000..f261bae59 --- /dev/null +++ b/tests/test_parametric_components/test_BlanketConstantThicknessArcH.py @@ -0,0 +1,59 @@ + +import unittest + +import paramak +import pytest + + +class TestBlanketConstantThicknessArcH(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.BlanketConstantThicknessArcH( + inner_lower_point=(300, -200), + inner_mid_point=(500, 0), + inner_upper_point=(300, 200), + thickness=20, + ) + + def test_default_parameters(self): + """Checks that the default parameters of a BlanketConstantThicknessArcH are correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.stp_filename == "BlanketConstantThicknessArcH.stp" + assert self.test_shape.stl_filename == "BlanketConstantThicknessArcH.stl" + assert self.test_shape.material_tag == "blanket_mat" + + def test_points_calculation(self): + """Checks that the points used to construct the BlanketConstantThicknessArcH component + are calculated correctly from the parameters given.""" + + assert self.test_shape.points == [ + (300, 200, 'circle'), (500, 0, 'circle'), (300, -200, 'straight'), (320, -200, 'circle'), + (520, 0, 'circle'), (320, 200, 'straight'), (300, 200, 'circle') + ] + + def test_component_creation(self): + """Creates a blanket using the BlanketConstantThicknessArcH parametric + component and checks that a cadquery solid is created.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + + def test_relative_shape_volume(self): + """Creates two blankets using the BlanketConstantThicknessArcH parametric component + and checks that their relative volumes are correct.""" + + test_volume = self.test_shape.volume + self.test_shape.rotation_angle = 180 + assert test_volume == pytest.approx(self.test_shape.volume * 2) + + def test_shape_face_areas(self): + """Creates a blanket using the BlanketConstantThicknessArcH parametric component and + checks that the face areas are expected.""" + + assert len(self.test_shape.areas) == 4 + assert len(set([round(i) for i in self.test_shape.areas])) == 3 + + self.test_shape.rotation_angle = 180 + assert len(self.test_shape.areas) == 6 + assert len(set([round(i) for i in self.test_shape.areas])) == 4 diff --git a/tests/test_parametric_components/test_BlanketConstantThicknessArcV.py b/tests/test_parametric_components/test_BlanketConstantThicknessArcV.py new file mode 100644 index 000000000..3ab3c2b1c --- /dev/null +++ b/tests/test_parametric_components/test_BlanketConstantThicknessArcV.py @@ -0,0 +1,64 @@ + +import unittest + +import paramak +import pytest + + +class TestBlanketConstantThicknessArcV(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.BlanketConstantThicknessArcV( + inner_lower_point=(300, -200), + inner_mid_point=(500, 0), + inner_upper_point=(300, 200), + thickness=20, + ) + + def test_default_parameters(self): + """Checks that the default parameters of a BlanketConstantThicknessArcV are correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.stp_filename == "BlanketConstantThicknessArcV.stp" + assert self.test_shape.stl_filename == "BlanketConstantThicknessArcV.stl" + assert self.test_shape.material_tag == "blanket_mat" + + def test_points_calculation(self): + """Checks that the points used to construct the BlanketConstantThicknessArcH component + are calculated correctly from the parameters given.""" + + assert self.test_shape.points == [ + (300, 200, 'circle'), + (500, 0, 'circle'), + (300, -200, 'straight'), + (300, -220, 'circle'), + (520, 0, 'circle'), + (300, 220, 'straight'), + (300, 200, 'circle') + ] + + def test_component_creation(self): + """Creates a blanekt using the BlanketConstantThicknessArcH parametric component and + checks that a cadquery solid is created.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + + def test_relative_shape_volume(self): + """Creates two blankets using the BlanketConstantThicknessArcV parametric component and + checks that their relative volumes are correct.""" + + test_volume = self.test_shape.volume + self.test_shape.rotation_angle = 180 + assert test_volume == pytest.approx(self.test_shape.volume * 2) + + def test_shape_face_areas(self): + """Creates a blanket using the BlanketConstantThicknessArcV parametric component and + checks that the face areas are expected.""" + + assert len(self.test_shape.areas) == 4 + assert len(set([round(i) for i in self.test_shape.areas])) == 3 + + self.test_shape.rotation_angle = 180 + assert len(self.test_shape.areas) == 6 + assert len(set([round(i) for i in self.test_shape.areas])) == 4 diff --git a/tests/test_parametric_components/test_BlanketCutterParallels.py b/tests/test_parametric_components/test_BlanketCutterParallels.py new file mode 100644 index 000000000..ca4233902 --- /dev/null +++ b/tests/test_parametric_components/test_BlanketCutterParallels.py @@ -0,0 +1,73 @@ + +import unittest + +import paramak + + +class TestBlanketCutterParallels(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.BlanketCutterParallels( + thickness=50, + gap_size=200 + ) + + def test_default_parameters(self): + """Checks that the default parameters of a BlanketCutterParallel are correct.""" + + assert self.test_shape.azimuth_placement_angle == [ + 0., 36., 72., 108., 144., 180., 216., 252., 288., 324.] + assert self.test_shape.height == 2000 + assert self.test_shape.width == 2000 + assert self.test_shape.stp_filename == "BlanketCutterParallels.stp" + assert self.test_shape.stl_filename == "BlanketCutterParallels.stl" + # assert self.test_shape.name == "blanket_cutter_parallels" + assert self.test_shape.material_tag == "blanket_cutter_parallels_mat" + + def test_creation(self): + """Creates solid using the BlanketCutterParallels parametric component + and checks that a cadquery solid is created.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + + # def test_BlanketCutterParallels_distance_volume_impact(self): + # """Creates solid using the BlanketCutterParallels parametric component + # with different distances and checks that the volume changes accordingly + # .""" + + # test_volume = self.test_shape.volume + # self.test_shape.thickness=100 + # assert test_volume < self.test_shape.volume + + def test_cut_modification(self): + """Creates a BlanketCutterParallels parametric component and with another + shape cut out and checks that a solid can be produced.""" + + cut_shape = paramak.ExtrudeCircleShape(1, 1, points=[(0, 0)]) + self.test_shape.cut = cut_shape + assert self.test_shape.solid is not None + assert self.test_shape.solid is not None + + def test_distance_is_modified(self): + test_shape = paramak.BlanketCutterParallels( + thickness=50, + gap_size=50, + ) + + for thickness, gap_size in zip([20, 30, 40], [10, 20, 30]): + test_shape.thickness = thickness + test_shape.gap_size = gap_size + assert test_shape.distance == test_shape.gap_size / 2 + test_shape.thickness + + def test_main_cutting_shape_is_modified(self): + test_shape = paramak.BlanketCutterParallels( + thickness=50, + gap_size=50, + ) + + for gap_size, angles in zip([10, 20, 30], [0, 1, 3]): + test_shape.gap_size = gap_size + test_shape.azimuth_placement_angle = angles + assert test_shape.main_cutting_shape.distance == test_shape.gap_size / 2.0 + assert test_shape.main_cutting_shape.azimuth_placement_angle == test_shape.azimuth_placement_angle diff --git a/tests/test_parametric_components/test_BlanketCutterStar.py b/tests/test_parametric_components/test_BlanketCutterStar.py new file mode 100644 index 000000000..7a9d6aff2 --- /dev/null +++ b/tests/test_parametric_components/test_BlanketCutterStar.py @@ -0,0 +1,50 @@ + +import unittest + +import paramak +import pytest + + +class TestBlanketCutterStar(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.BlanketCutterStar(distance=100) + + def test_default_parameters(self): + """Checks that the default parameters of a BlanketCutterStar are correct.""" + + assert self.test_shape.azimuth_placement_angle == [ + 0., 36., 72., 108., 144., 180., 216., 252., 288., 324.] + assert self.test_shape.height == 2000 + assert self.test_shape.width == 2000 + assert self.test_shape.stp_filename == "BlanketCutterStar.stp" + assert self.test_shape.stl_filename == "BlanketCutterStar.stl" + assert self.test_shape.name == "blanket_cutter_star" + assert self.test_shape.material_tag == "blanket_cutter_star_mat" + + def test_points_calculation(self): + """Checks that the points used to construct the BlanketCutterStar component + are calculated correctly from the parameters given.""" + + assert self.test_shape.points == [(0, - + 1000, "straight"), (2000, - + 1000, "straight"), (2000, 1000, "straight"), (0, 1000, "straight"), (0, - + 1000, "straight")] + + def test_creation(self): + """Creates a solid using the BlanketCutterStar parametric component + and checks that a cadquery solid is created.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + + def test_distance_volume_impact(self): + """Creates solid using the BlanketCutterStar parametric component + with different distances and checks that the volume changes accordingly + .""" + + test_volume = self.test_shape.volume + self.test_shape.distance = 50 + # not quite two times as large as there is overlap in the center + assert test_volume == pytest.approx( + self.test_shape.volume * 2, rel=0.1) diff --git a/tests/test_parametric_components/test_BlanketFP.py b/tests/test_parametric_components/test_BlanketFP.py new file mode 100644 index 000000000..dec893b51 --- /dev/null +++ b/tests/test_parametric_components/test_BlanketFP.py @@ -0,0 +1,217 @@ + +import os +import unittest +import warnings +from pathlib import Path + +import paramak + + +class TestBlanketFP(unittest.TestCase): + + def setUp(self): + self.plasma = paramak.Plasma( + major_radius=450, + minor_radius=150, + triangularity=0.55, + elongation=2 + ) + + self.test_shape = paramak.BlanketFP( + thickness=150, + start_angle=-90, + stop_angle=240, + ) + + def test_default_parameters(self): + """Checks that the default parameters of a BlanketFP are correct.""" + + assert self.test_shape.plasma is None + assert self.test_shape.minor_radius == 150 + assert self.test_shape.major_radius == 450 + assert self.test_shape.triangularity == 0.55 + assert self.test_shape.elongation == 2 + assert self.test_shape.vertical_displacement == 0 + assert self.test_shape.offset_from_plasma == 0 + assert self.test_shape.num_points == 50 + assert self.test_shape.stp_filename == "BlanketFP.stp" + assert self.test_shape.stl_filename == "BlanketFP.stl" + assert self.test_shape.material_tag == "blanket_mat" + + def test_creation_plasma(self): + """Checks that a cadquery solid can be created by passing a plasma to + the BlanketFP parametric component.""" + + self.test_shape.plasma = self.plasma + self.test_shape.offset_from_plasma = 30 + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + + def test_faces(self): + """creates a blanket using the BlanketFP parametric component and checks + that a solid with the correct number of faces is created""" + + self.test_shape.plasma = self.plasma + self.test_shape.offset_from_plasma = 30 + + assert len(self.test_shape.areas) == 4 + assert len(set([round(i) for i in self.test_shape.areas])) == 4 + + self.test_shape.rotation_angle = 180 + assert len(self.test_shape.areas) == 6 + assert len(set([round(i) for i in self.test_shape.areas])) == 5 + + def test_creation_noplasma(self): + """Checks that a cadquery solid can be created using the BlanketFP + parametric component when no plasma is passed.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + + def test_creation_variable_thickness_from_tuple(self): + """Checks that a cadquery solid can be created using the BlanketFP + parametric component when a tuple of thicknesses is passed as an + argument.""" + + self.test_shape.thickness = (100, 200) + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + + def test_creation_variable_thickness_from_2_lists(self): + """Checks that a cadquery solid can be created using the BlanketFP + parametric component when a list of angles and a list of thicknesses + are passed as an argument.""" + + self.test_shape.thickness = [(-90, 240), [10, 30]] + + assert self.test_shape.solid is not None + + def test_creation_variable_thickness_function(self): + """Checks that a cadquery solid can be created using the BlanketFP + parametric component when a thickness function is passed as an + argument.""" + + def thickness(theta): + return 10 + 0.1 * theta + + self.test_shape.thickness = thickness + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + + def test_creation_variable_offset_from_tuple(self): + """Checks that a cadquery solid can be created using the BlanketFP + parametric component when a tuple of offsets is passed as an + argument.""" + + self.test_shape.offset_from_plasma = (0, 10) + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + + def test_creation_variable_offset_from_2_lists(self): + """Checks that a cadquery solid can be created using the BlanketFP + parametric component when a list of offsets and a list of angles are + passed as an argument.""" + + self.test_shape.start_angle = 90 + self.test_shape.stop_angle = 270 + self.test_shape.offset_from_plasma = [[270, 100, 90], [0, 5, 10]] + + assert self.test_shape.solid is not None + + def test_creation_variable_offset_error(self): + """Checks that an error is raised when two lists with different + lengths are passed in offset_from_plasma as an argument.""" + + def test_different_lengths(): + self.test_shape.start_angle = 90 + self.test_shape.stop_angle = 270 + self.test_shape.offset_from_plasma = [ + [270, 100, 90], [0, 5, 10, 15]] + self.test_shape.solid + + self.assertRaises(ValueError, test_different_lengths) + + def test_creation_variable_offset_function(self): + """Checks that a cadquery solid can be created using the BlanketFP + parametric component when an offset function is passed.""" + + def offset(theta): + return 10 + 0.1 * theta + + self.test_shape.offset_from_plasma = offset + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + + def test_physical_groups(self): + """Creates a blanket using the BlanketFP parametric component and + checks that physical groups can be exported using the + export_physical_groups method""" + + outfile = "tests/blanket.json" + + # 180 coverage, full rotation + test_shape = paramak.BlanketFP(100, stop_angle=180, start_angle=0,) + test_shape.export_physical_groups(outfile) + + # full coverage, 180 rotation + test_shape = paramak.BlanketFP( + 100, stop_angle=0, start_angle=360, + rotation_angle=180) + test_shape.export_physical_groups(outfile) + + # 180 coverage, 180 rotation + test_shape = paramak.BlanketFP( + 100, stop_angle=180, start_angle=0, + rotation_angle=180) + test_shape.export_physical_groups(outfile) + os.system("rm " + outfile) + + def test_full_cov_stp_export(self): + """Creates a blanket using the BlanketFP parametric component with full + coverage and checks that an stp file can be exported using the export_stp + method.""" + + self.test_shape.rotation_angle = 180 + self.test_shape.start_angle = 0 + self.test_shape.stop_angle = 360 + + self.test_shape.export_stp("test_blanket_full_cov.stp") + assert Path("test_blanket_full_cov.stp").exists() + os.system("rm test_blanket_full_cov.stp") + + def test_full_cov_full_rotation(self): + """Creates a blanket using the BlanketFP parametric component with full + coverage and full rotation and checks that an stp file can be exported using + the export_stp method.""" + + self.test_shape.rotation_angle = 360 + self.test_shape.start_angle = 0 + self.test_shape.stop_angle = 360 + + self.test_shape.export_stp("test_blanket_full_cov_full_rot.stp") + assert Path("test_blanket_full_cov_full_rot.stp").exists() + os.system("rm test_blanket_full_cov_full_rot.stp") + + def test_overlapping(self): + """Creates an overlapping geometry and checks that a warning is raised. + """ + + test_shape = paramak.BlanketFP( + major_radius=100, + minor_radius=100, + triangularity=0.5, + elongation=2, + thickness=200, + stop_angle=360, + start_angle=0, + ) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + assert test_shape.solid is not None + assert len(w) == 1 diff --git a/tests/test_parametric_components/test_CenterColumnShieldCircular.py b/tests/test_parametric_components/test_CenterColumnShieldCircular.py new file mode 100644 index 000000000..a03aed797 --- /dev/null +++ b/tests/test_parametric_components/test_CenterColumnShieldCircular.py @@ -0,0 +1,50 @@ + +import unittest + +import paramak + + +class TestCenterColumnShieldCircular(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.CenterColumnShieldCircular( + height=600, inner_radius=100, mid_radius=150, outer_radius=200 + ) + + def test_default_parameters(self): + """Checks that the default parameters of a CenterColumnShieldCircular are correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.stp_filename == "CenterColumnShieldCircular.stp" + assert self.test_shape.stl_filename == "CenterColumnShieldCircular.stl" + # assert self.test_shape.name == "center_column_shield" + assert self.test_shape.material_tag == "center_column_shield_mat" + + def test_points_calculation(self): + """Checks that the points used to construct the CenterColumnShieldCircular components + are calculated correctly from the parameters given.""" + + assert self.test_shape.points == [ + (100, 0, 'straight'), (100, 300.0, 'straight'), (200, 300.0, 'circle'), + (150, 0, 'circle'), (200, -300.0, 'straight'), (100, -300.0, 'straight'), + (100, 0, 'straight') + ] + + def test_creation(self): + """Creates a center column shield using the CenterColumnShieldCircular + parametric component and checks that a cadquery solid is created.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + + def test_faces(self): + """Creates a center column shield using the CenterColumnShieldCircular + parametric component and checks that a solid is created with the correct + number of faces""" + + assert len(self.test_shape.areas) == 4 + assert len(set([round(i) for i in self.test_shape.areas])) == 3 + + self.test_shape.rotation_angle = 180 + assert len(self.test_shape.areas) == 6 + assert len(set([round(i) for i in self.test_shape.areas])) == 4 diff --git a/tests/test_parametric_components/test_CenterColumnShieldCylinder.py b/tests/test_parametric_components/test_CenterColumnShieldCylinder.py new file mode 100644 index 000000000..51d323dd4 --- /dev/null +++ b/tests/test_parametric_components/test_CenterColumnShieldCylinder.py @@ -0,0 +1,123 @@ + +import math +import os +import unittest +from pathlib import Path + +import paramak +import pytest + + +class TestCenterColumnShieldCylinder(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.CenterColumnShieldCylinder( + height=600, inner_radius=100, outer_radius=200 + ) + + def test_default_parameters(self): + """Checks that the default parameters of a CenterColumnShieldCylinder are correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.stp_filename == "CenterColumnShieldCylinder.stp" + assert self.test_shape.stl_filename == "CenterColumnShieldCylinder.stl" + # assert self.test_shape.name == "center_column_shield" + assert self.test_shape.material_tag == "center_column_shield_mat" + + def test_creation(self): + """Creates a center column shield using the CenterColumnShieldCylinder + parametric component and checks that a cadquery solid is created.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + + def test_points_calculation(self): + """Checks that the points used to construct the CenterColumnShieldCylinder component + are calculated correctly from the parameters given.""" + + assert self.test_shape.points == [ + (100, 300, "straight"), (200, 300, "straight"), (200, -300, "straight"), + (100, -300, "straight"), (100, 300, "straight") + ] + + def test_relative_volume(self): + """Creates CenterColumnShieldCylinder shapes and checks that their + relative volumes are correct""" + + test_volume = self.test_shape.volume + self.test_shape.rotation_angle = 180 + assert test_volume == pytest.approx(self.test_shape.volume * 2) + + def test_absolute_volume(self): + """Creates a CenterColumnShieldCylinder shape and checks that its + relative volume is correct""" + + assert self.test_shape.volume == pytest.approx( + ((math.pi * (200**2)) - (math.pi * (100**2))) * 600 + ) + + def test_absolute_area(self): + """Creates a CenterColumnShieldCylinder shape and checks that the + areas of the faces of the solid created are correct""" + + assert len(self.test_shape.areas) == 4 + assert self.test_shape.area == pytest.approx((((math.pi * (200**2)) - (math.pi * ( + 100**2))) * 2) + (math.pi * (2 * 200) * 600) + (math.pi * (2 * 100) * 600)) + assert self.test_shape.areas.count(pytest.approx( + (math.pi * (200**2)) - (math.pi * (100**2)))) == 2 + assert self.test_shape.areas.count( + pytest.approx(math.pi * (2 * 200) * 600)) == 1 + assert self.test_shape.areas.count( + pytest.approx(math.pi * (2 * 100) * 600)) == 1 + + def test_export_stp_CenterColumnShieldCylinder(self): + """Creates a CenterColumnShieldCylinder shape and checks that a stp + file of the shape can be exported using the export_stp method.""" + + os.system("rm center_column_shield.stp") + self.test_shape.export_stp("center_column_shield.stp") + assert Path("center_column_shield.stp").exists() + os.system("rm center_column_shield.stp") + + def test_parametric_component_hash_value(self): + """Creates a parametric component and checks that a cadquery solid with + a unique hash value is created when .solid is called. Checks that the + same cadquery solid with the same unique hash value is returned when + shape.solid is called again after no changes have been made to the + parametric component. Checks that a new cadquery solid with a new + unique hash value is constructed when shape.solid is called after + changes to the parametric component have been made. Checks that the + hash_value of a parametric component is not updated until a new + cadquery solid has been created.""" + + assert self.test_shape.hash_value is None + assert self.test_shape.solid is not None + assert self.test_shape.hash_value is not None + initial_hash_value = self.test_shape.hash_value + assert self.test_shape.solid is not None + assert initial_hash_value == self.test_shape.hash_value + self.test_shape.height = 120 + assert initial_hash_value == self.test_shape.hash_value + assert self.test_shape.solid is not None + assert initial_hash_value != self.test_shape.hash_value + + def test_center_column_shield_cylinder_invalid_parameters_errors(self): + """Checks that the correct errors are raised when invalid arguments are entered + as shape parameters.""" + + def incorrect_inner_radius(): + self.test_shape.inner_radius = self.test_shape.outer_radius + 1 + self.test_shape.outer_radius = 20 + self.test_shape.inner_radius = 40 + + def incorrect_outer_radius(): + self.test_shape.outer_radius = self.test_shape.inner_radius + 1 + self.test_shape.inner_radius = 40 + self.test_shape.outer_radius = 20 + + def incorrect_height(): + self.test_shape.height = None + + self.assertRaises(ValueError, incorrect_inner_radius) + self.assertRaises(ValueError, incorrect_outer_radius) + self.assertRaises(ValueError, incorrect_height) diff --git a/tests/test_parametric_components/test_CenterColumnShieldFlatTopCircular.py b/tests/test_parametric_components/test_CenterColumnShieldFlatTopCircular.py new file mode 100644 index 000000000..da4a5981a --- /dev/null +++ b/tests/test_parametric_components/test_CenterColumnShieldFlatTopCircular.py @@ -0,0 +1,51 @@ + +import unittest + +import paramak + + +class TestCenterColumnShieldFlatTopCircular(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.CenterColumnShieldFlatTopCircular( + height=600, arc_height=400, inner_radius=100, mid_radius=150, outer_radius=200) + + def test_default_parameters(self): + """Checks that the default parameters of a CenterColumnShieldFlatTopCircular are + correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.stp_filename == "CenterColumnShieldFlatTopCircular.stp" + assert self.test_shape.stl_filename == "CenterColumnShieldFlatTopCircular.stl" + # assert self.test_shape.name == "center_column" + assert self.test_shape.material_tag == "center_column_shield_mat" + + def test_points_calculation(self): + """Checks that the points used to construct the CenterColumnShieldFlatTopCircular + component are calculated correctly from the parameters given.""" + + assert self.test_shape.points == [ + (100, 0, "straight"), (100, 300, "straight"), (200, 300, "straight"), + (200, 200, "circle"), (150, 0, "circle"), (200, -200, "straight"), + (200, -300, "straight"), (100, -300, "straight"), (100, 0, "straight") + ] + + def test_creation(self): + """Creates a center column shield using the + CenterColumnShieldFlatTopCircular parametric component and checks that + a cadquery solid is created.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + + def test_faces(self): + """Creates a center column shield using the + CenterColumnShieldFlatTopCircular parametric component and checks that + a solid is created with the correct number of faces""" + + assert len(self.test_shape.areas) == 6 + assert len(set([round(i) for i in self.test_shape.areas])) == 4 + + self.test_shape.rotation_angle = 180 + assert len(self.test_shape.areas) == 8 + assert len(set([round(i) for i in self.test_shape.areas])) == 5 diff --git a/tests/test_parametric_components/test_CenterColumnShieldFlatTopHyperbola.py b/tests/test_parametric_components/test_CenterColumnShieldFlatTopHyperbola.py new file mode 100644 index 000000000..b6ba864ee --- /dev/null +++ b/tests/test_parametric_components/test_CenterColumnShieldFlatTopHyperbola.py @@ -0,0 +1,77 @@ + +import unittest + +import paramak + + +class TestCenterColumnShieldFlatTopHyperbola(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.CenterColumnShieldFlatTopHyperbola( + height=600, arc_height=400, inner_radius=100, mid_radius=150, outer_radius=200) + + def test_default_parameters(self): + """Checks that the default parameters of a CenterColumnShieldFlatTopHyperbola are + correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.stp_filename == "CenterColumnShieldFlatTopHyperbola.stp" + assert self.test_shape.stl_filename == "CenterColumnShieldFlatTopHyperbola.stl" + # assert self.test_shape.name == "center_column" + assert self.test_shape.material_tag == "center_column_shield_mat" + + def test_points_calculation(self): + """Checks that the points used to construct the CenterColumnShieldFlatTopHyperbola + component are calculated correctly from the parameters given.""" + + assert self.test_shape.points == [ + (100, 0, "straight"), (100, 300, "straight"), (200, 300, "straight"), + (200, 200, "spline"), (150, 0, "spline"), (200, -200, "straight"), + (200, -300, "straight"), (100, -300, "straight"), (100, 0, "straight") + ] + + def test_creation(self): + """Creates a center column shield using the + CenterColumnShieldFlatTopHyperbola parametric component and checks that + a cadquery solid is created.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + + def test_invalid_parameters_errors( + self): + """Checks that the correct errors are raised when invalid arguments are input as + shape parameters.""" + + def incorrect_inner_radius(): + self.test_shape.inner_radius = 220 + self.test_shape.solid + + def incorrect_mid_radius(): + self.test_shape.mid_radius = 220 + self.test_shape.solid + + def incorrect_outer_radius(): + self.test_shape.outer_radius = 130 + self.test_shape.solid + + def incorrect_arc_height(): + self.test_shape.arc_height = 700 + self.test_shape.solid + + self.assertRaises(ValueError, incorrect_inner_radius) + self.assertRaises(ValueError, incorrect_mid_radius) + self.assertRaises(ValueError, incorrect_outer_radius) + self.assertRaises(ValueError, incorrect_arc_height) + + def test_faces(self): + """Creates a center column shield using the + CenterColumnShieldFlatTopHyperbola parametric component and checks + that a solid is created with the correct number of faces.""" + + assert len(self.test_shape.areas) == 6 + assert len(set([round(i) for i in self.test_shape.areas])) == 4 + + self.test_shape.rotation_angle = 180 + assert len(self.test_shape.areas) == 8 + assert len(set([round(i) for i in self.test_shape.areas])) == 5 diff --git a/tests/test_parametric_components/test_CenterColumnShieldHyperbola.py b/tests/test_parametric_components/test_CenterColumnShieldHyperbola.py new file mode 100644 index 000000000..b9fe61704 --- /dev/null +++ b/tests/test_parametric_components/test_CenterColumnShieldHyperbola.py @@ -0,0 +1,72 @@ + +import unittest + +import paramak + + +class TestCenterColumnShieldHyperbola(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.CenterColumnShieldHyperbola( + height=600, inner_radius=100, mid_radius=150, outer_radius=200 + ) + + def test_default_parameters(self): + """Checks that the default parameters of a CenterColumnShieldHyperbola are + correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.stp_filename == "CenterColumnShieldHyperbola.stp" + assert self.test_shape.stl_filename == "CenterColumnShieldHyperbola.stl" + # assert self.test_shape.name == "center_column" + assert self.test_shape.material_tag == "center_column_shield_mat" + + def test_points_calculation(self): + """Checks that the points used to construct the CenterColumnShieldHyperbola component + are calculated correctly from the parameters given.""" + + assert self.test_shape.points == [ + (100.0, 0.0, "straight"), (100.0, 300.0, "straight"), (200.0, 300.0, "spline"), + (150.0, 0.0, "spline"), (200.0, -300.0, "straight"), (100.0, -300.0, "straight"), + (100.0, 0.0, "straight") + ] + + def test_creation(self): + """Creates a center column shield using the + CenterColumnShieldHyperbola parametric component and checks that a + cadquery solid is created.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + + def test_invalid_parameters_errors(self): + """Checks that the correct errors are raised when invalid arguments are input + as shape parameters.""" + + def incorrect_inner_radius(): + self.test_shape.inner_radius = 180 + self.test_shape.solid + + def incorrect_mid_radius(): + self.test_shape.mid_radius = 80 + self.test_shape.solid + + def incorrect_outer_radius(): + self.test_shape.outer_radius = 130 + self.test_shape.solid + + self.assertRaises(ValueError, incorrect_inner_radius) + self.assertRaises(ValueError, incorrect_mid_radius) + self.assertRaises(ValueError, incorrect_outer_radius) + + def test_faces(self): + """Creates a center column shield using the CenterColumnShieldHyperbola + parametric component and checks that a solid with the correct number of + faces is created""" + + assert len(self.test_shape.areas) == 4 + assert len(set([round(i) for i in self.test_shape.areas])) == 3 + + self.test_shape.rotation_angle = 180 + assert len(self.test_shape.areas) == 6 + assert len(set([round(i) for i in self.test_shape.areas])) == 4 diff --git a/tests/test_parametric_components/test_CenterColumnShieldPlasmaHyperbola.py b/tests/test_parametric_components/test_CenterColumnShieldPlasmaHyperbola.py new file mode 100644 index 000000000..480688183 --- /dev/null +++ b/tests/test_parametric_components/test_CenterColumnShieldPlasmaHyperbola.py @@ -0,0 +1,71 @@ + +import unittest + +import paramak + + +class TestCenterColumnShieldPlasmaHyperbola(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.CenterColumnShieldPlasmaHyperbola( + height=800, inner_radius=100, mid_offset=40, edge_offset=30 + ) + + def test_default_parameters(self): + """Checks that the default parameters of a CenterColumnShieldPlasmaHyperbola are + correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.major_radius == 450 + assert self.test_shape.minor_radius == 150 + assert self.test_shape.triangularity == 0.55 + assert self.test_shape.elongation == 2 + assert self.test_shape.stp_filename == "CenterColumnShieldPlasmaHyperbola.stp" + assert self.test_shape.stl_filename == "CenterColumnShieldPlasmaHyperbola.stl" + # assert self.test_shape.name == "center_column" + assert self.test_shape.material_tag == "center_column_shield_mat" + + def test_creation(self): + """Creates a center column shield using the + CenterColumnShieldPlasmaHyperbola parametric component and checks that + a cadquery solid is created.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + + def test_points_calculation(self): + """Checks that the points used to construct the CenterColumnShieldPlasmaHyperbola + component are calculated correctly fro the parameters given.""" + + assert self.test_shape.points == [ + (100, 0, 'straight'), (100, 400.0, 'straight'), (337.5, 400.0, 'straight'), + (337.5, 300.0, 'spline'), (260.0, 0.0, 'spline'), (337.5, -300.0, 'straight'), + (337.5, -400.0, 'straight'), (100, -400.0, 'straight'), (100, 0, 'straight') + ] + + def test_invalid_parameters_errors(self): + """Checks that the correct errors are raised when invalid arguments are input as + shape parameters.""" + + def incorrect_inner_radius(): + self.test_shape.inner_radius = 601 + self.test_shape.solid + + def incorrect_height(): + self.test_shape.height = 301 + self.test_shape.solid + + self.assertRaises(ValueError, incorrect_inner_radius) + self.assertRaises(ValueError, incorrect_height) + + def test_faces(self): + """Creates a center column shield using the CenterColumnShieldPlasmaHyperbola + parametric component and checks that a solid with the correct number of + faces is created""" + + assert len(self.test_shape.areas) == 6 + assert len(set([round(i) for i in self.test_shape.areas])) == 4 + + self.test_shape.rotation_angle = 180 + assert len(self.test_shape.areas) == 8 + assert len(set([round(i) for i in self.test_shape.areas])) == 5 diff --git a/tests/test_parametric_components/test_CoolantChannelRingCurved.py b/tests/test_parametric_components/test_CoolantChannelRingCurved.py new file mode 100644 index 000000000..20b32c4fc --- /dev/null +++ b/tests/test_parametric_components/test_CoolantChannelRingCurved.py @@ -0,0 +1,59 @@ + +import unittest + +import paramak +import pytest + + +class TestCoolantChannelRingCurved(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.CoolantChannelRingCurved( + height=100, + channel_radius=10, + ring_radius=70, + mid_offset=-20, + number_of_coolant_channels=6 + ) + + def test_default_parameters(self): + """Checks that the default parameters of a CoolantChannelRingCurved are correct.""" + + # assert self.test_shape.rotation_angle == 360 + assert self.test_shape.start_angle == 0 + assert self.test_shape.stp_filename == "CoolantChannelRingCurved.stp" + assert self.test_shape.stl_filename == "CoolantChannelRingCurved.stl" + assert self.test_shape.material_tag == "coolant_channel_mat" + + def test_creation(self): + """Creates a coolant channel ring using the CoolantChannelRingCurved parametric shape + and checks that a cadquery solid is created.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + + def test_relative_volumes(self): + """Creates coolant channel rings using the CoolantChannelRingCurved parametric shape + and checks the relative volumes are correct.""" + + test_volume = self.test_shape.volume + self.test_shape.number_of_coolant_channels = 3 + assert test_volume == pytest.approx(self.test_shape.volume * 2) + + test_volume = self.test_shape.volume + self.test_shape.mid_offset = -30 + assert test_volume > self.test_shape.volume + self.test_shape.force_cross_section = True + assert test_volume < self.test_shape.volume + + def test_start_angle(self): + """Checks that the coolant channels are placed at the correct azimuthal placement + angles for a given start angle.""" + + assert self.test_shape.azimuth_placement_angle == [ + 0, 60, 120, 180, 240, 300 + ] + self.test_shape.start_angle = 10 + assert self.test_shape.azimuth_placement_angle == [ + 10, 70, 130, 190, 250, 310 + ] diff --git a/tests/test_parametric_components/test_CoolantChannelRingStraight.py b/tests/test_parametric_components/test_CoolantChannelRingStraight.py new file mode 100644 index 000000000..63a8ae7ca --- /dev/null +++ b/tests/test_parametric_components/test_CoolantChannelRingStraight.py @@ -0,0 +1,69 @@ + +import math +import unittest + +import paramak +import pytest + + +class TestCoolantChannelRingStraight(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.CoolantChannelRingStraight( + height=100, + channel_radius=10, + ring_radius=70, + number_of_coolant_channels=8 + ) + + def test_default_parameters(self): + """Checks that the default parameters of a CoolantChannelRingStraight are correct.""" + + # assert self.test_shape.rotation_angle == 360 + assert self.test_shape.start_angle == 0 + assert self.test_shape.stp_filename == "CoolantChannelRingStraight.stp" + assert self.test_shape.stl_filename == "CoolantChannelRingStraight.stl" + assert self.test_shape.material_tag == "coolant_channel_mat" + + def test_creation(self): + """Creates a coolant channel ring using the CoolantChannelRingStraight parameteric shape + and checks that a cadquery solid is created.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + + def test_faces(self): + """Creates a CoolantChannelRingStraight shape and checks that the areas of its faces + are correct.""" + + self.test_shape.workplane = "XY" + self.test_shape.rotation_axis = "Z" + + assert self.test_shape.area == pytest.approx( + (((math.pi * (10**2)) * 2) + (math.pi * (10 * 2) * 100)) * 8) + assert len(self.test_shape.areas) == 24 + assert self.test_shape.areas.count( + pytest.approx(math.pi * (10**2))) == 16 + assert self.test_shape.areas.count( + pytest.approx(math.pi * (10 * 2) * 100)) == 8 + + def test_volume(self): + """Creates CoolantChannelRingStraight shapes and checks that the volumes are correct.""" + + self.test_shape.workplane = "XY" + self.test_shape.rotation_axis = "Z" + + assert self.test_shape.volume == pytest.approx( + math.pi * (10 ** 2) * 100 * 8) + + def test_start_angle(self): + """Checks that the coolant channels are placed at the correct azimuthal placement + angles for a given start angle.""" + + assert self.test_shape.azimuth_placement_angle == [ + 0, 45, 90, 135, 180, 225, 270, 315 + ] + self.test_shape.start_angle = 10 + assert self.test_shape.azimuth_placement_angle == [ + 10, 55, 100, 145, 190, 235, 280, 325 + ] diff --git a/tests/test_parametric_components/test_CuttingWedge.py b/tests/test_parametric_components/test_CuttingWedge.py new file mode 100644 index 000000000..39db4d17e --- /dev/null +++ b/tests/test_parametric_components/test_CuttingWedge.py @@ -0,0 +1,28 @@ + +import math +import random +import unittest + +import paramak +import pytest + + +class TestCuttingWedge(unittest.TestCase): + """Creates a random sized cutting wedge and changes the volume""" + + def test_volume_of_for_5_random_dimentions(self): + for test_number in range(5): + height = random.uniform(1., 2000.) + radius = random.uniform(1., 1000) + rotation_angle = random.uniform(1., 360.) + azimuth_placement_angle = random.uniform(1., 360.) + + test_shape = paramak.CuttingWedge( + height=height, + radius=radius, + rotation_angle=rotation_angle, + azimuth_placement_angle=azimuth_placement_angle + ) + angle_fraction = 360 / rotation_angle + correct_volume = (math.pi * radius ** 2 * height) / angle_fraction + assert test_shape.volume == pytest.approx(correct_volume) diff --git a/tests/test_parametric_components/test_CuttingWedgeFS.py b/tests/test_parametric_components/test_CuttingWedgeFS.py new file mode 100644 index 000000000..b199cb04c --- /dev/null +++ b/tests/test_parametric_components/test_CuttingWedgeFS.py @@ -0,0 +1,74 @@ + +import unittest + +import numpy as np +import paramak + + +class TestCuttingWedgeFS(unittest.TestCase): + + def test_shape_construction_and_volume(self): + """Makes cutting cylinders from shapes and checks the + volume of the cutter shape is larger than the shape it + encompasses.""" + + hoop_shape = paramak.PoloidalFieldCoil(height=20, + width=20, + center_point=(50, 200), + rotation_angle=180) + + cutter = paramak.CuttingWedgeFS( + shape=hoop_shape, + azimuth_placement_angle=0, + ) + + assert cutter.volume > hoop_shape.volume + + def test_error(self): + """Checks that errors are raised when invalid arguments are set + """ + shape = paramak.ExtrudeStraightShape( + 1, + points=[(0, 0), (0, 1), (1, 1)], + rotation_angle=180 + ) + cutter = paramak.CuttingWedgeFS( + shape=shape, + azimuth_placement_angle=0, + ) + + def incorrect_rotation_angle(): + shape.rotation_angle = 360 + print(cutter.shape.rotation_angle) + cutter.solid + + def incorrect_shape_points(): + shape.rotation_angle = 180 + cutter.shape.points = [(0, 0, 'straight')] + print(shape.points) + cutter.solid + + def incorrect_shape_rotation_angle(): + shape.rotation_angle = 360 + cutter.shape = shape + + self.assertRaises(ValueError, incorrect_rotation_angle) + self.assertRaises(ValueError, incorrect_shape_points) + self.assertRaises(ValueError, incorrect_shape_rotation_angle) + + def test_different_workplanes(self): + """Test that checks the cutting wedge can be correctly applied to a + shape with non-default workplane and rotation_axis + """ + rectangle = paramak.ExtrudeStraightShape( + 2, + points=[(-0.5, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5)], + workplane="XY", + rotation_axis="Z" + ) + rectangle.rotation_angle = 360 + volume_full = rectangle.volume + assert np.isclose(volume_full, 2) + rectangle.rotation_angle = 90 + volume_quarter = rectangle.volume + assert np.isclose(volume_quarter, 0.5) diff --git a/tests/test_parametric_components/test_DivertorITER.py b/tests/test_parametric_components/test_DivertorITER.py new file mode 100644 index 000000000..01fb49e06 --- /dev/null +++ b/tests/test_parametric_components/test_DivertorITER.py @@ -0,0 +1,35 @@ + +import unittest + +import paramak + + +class TestDivertorITER(unittest.TestCase): + + def test_creation(self): + """Creates an ITER-type divertor using the ITERtypeDivertor parametric + component and checks that a cadquery solid is created""" + + test_shape = paramak.ITERtypeDivertor() + assert test_shape.solid is not None + + def test_stp_export(self): + """Creates an ITER-type divertor using the ITERtypeDivertor parametric + component and checks that a stp file of the shape can be exported using + the export_stp method""" + + test_shape = paramak.ITERtypeDivertor() + test_shape.export_stp("tests/ITER_div") + + def test_faces(self): + """Creates an ITER-type divertor using the ITERtypeDivertor parametric + component and checks that a solid with the correct number of faces is + created""" + + test_shape = paramak.ITERtypeDivertor() + assert len(test_shape.areas) == 12 + assert len(set(test_shape.areas)) == 12 + + test_shape.rotation_angle = 180 + assert len(test_shape.areas) == 14 + assert len(set(test_shape.areas)) == 13 diff --git a/tests/test_parametric_components/test_DivertorITERNoDome.py b/tests/test_parametric_components/test_DivertorITERNoDome.py new file mode 100644 index 000000000..a2119586f --- /dev/null +++ b/tests/test_parametric_components/test_DivertorITERNoDome.py @@ -0,0 +1,35 @@ + +import unittest + +import paramak + + +class TestDivertorITERNoDome(unittest.TestCase): + + def test_creation(self): + """Creates an ITER-type divertor using the ITERtypeDivertorNoDome + parametric component and checks that a cadquery solid is created.""" + + test_shape = paramak.ITERtypeDivertorNoDome() + assert test_shape.solid is not None + + def test_stp_export(self): + """Creates an ITER-type divertor using the ITERtypeDivertorNoDome + parametric component and checks that a stp file of the shape can be + exported using the export_stp method.""" + + test_shape = paramak.ITERtypeDivertorNoDome() + test_shape.export_stp("tests/ITER_div_no_dome") + + def test_faces(self): + """Creates an ITER-type divertor using the ITERtypeDivertorNoDome + parametric component and checks that a solid with the correct number + of faces is created""" + + test_shape = paramak.ITERtypeDivertorNoDome() + assert len(test_shape.areas) == 10 + assert len(set(test_shape.areas)) == 10 + + test_shape.rotation_angle = 180 + assert len(test_shape.areas) == 12 + assert len(set(test_shape.areas)) == 11 diff --git a/tests/test_parametric_components/test_InboardFirstwallFCCS.py b/tests/test_parametric_components/test_InboardFirstwallFCCS.py new file mode 100644 index 000000000..1e6dd2c18 --- /dev/null +++ b/tests/test_parametric_components/test_InboardFirstwallFCCS.py @@ -0,0 +1,193 @@ + +import unittest + +import numpy as np +import paramak + + +class TestInboardFirstwallFCCS(unittest.TestCase): + + def test_construction_with_CenterColumnShieldCylinder(self): + """Makes a firstwall from a CenterColumnShieldCylinder and checks + the volume is smaller than the shield.""" + + a = paramak.CenterColumnShieldCylinder( + height=100, + inner_radius=20, + outer_radius=80) + b = paramak.InboardFirstwallFCCS( + central_column_shield=a, + thickness=20, + rotation_angle=180) + assert a.solid is not None + assert b.solid is not None + assert a.volume > b.volume + + def test_construction_with_CenterColumnShieldHyperbola(self): + """Makes a firstwall from a CenterColumnShieldHyperbola and checks + the volume is smaller than the shield.""" + + a = paramak.CenterColumnShieldHyperbola( + height=200, + inner_radius=20, + mid_radius=80, + outer_radius=120) + b = paramak.InboardFirstwallFCCS( + central_column_shield=a, + thickness=20, + rotation_angle=180) + assert a.solid is not None + assert b.solid is not None + assert a.volume > b.volume + + def test_construction_with_CenterColumnShieldFlatTopHyperbola(self): + """Makes a firstwall from a CenterColumnShieldFlatTopHyperbola and + checks the volume is smaller than the shield.""" + + a = paramak.CenterColumnShieldFlatTopHyperbola( + height=200, + arc_height=100, + inner_radius=50, + mid_radius=80, + outer_radius=100) + b = paramak.InboardFirstwallFCCS( + central_column_shield=a, + thickness=20, + rotation_angle=180) + assert a.solid is not None + assert b.solid is not None + assert a.volume > b.volume + + def test_construction_with_CenterColumnShieldPlasmaHyperbola(self): + """Makes a firstwall from a CenterColumnShieldPlasmaHyperbola and + checks the volume is smaller than the shield.""" + + a = paramak.CenterColumnShieldPlasmaHyperbola( + height=601, + inner_radius=20, + mid_offset=50, + edge_offset=0) + b = paramak.InboardFirstwallFCCS( + central_column_shield=a, + thickness=20, + rotation_angle=180) + assert a.solid is not None + assert b.solid is not None + assert a.volume > b.volume + + def test_construction_with_CenterColumnShieldCircular(self): + """Makes a firstwall from a CenterColumnShieldCircular and checks + the volume is smaller than the shield.""" + + a = paramak.CenterColumnShieldCircular( + height=300, + inner_radius=20, + mid_radius=50, + outer_radius=100) + b = paramak.InboardFirstwallFCCS( + central_column_shield=a, + thickness=20, + rotation_angle=180) + assert a.solid is not None + assert b.solid is not None + assert a.volume > b.volume + + def test_construction_with_CenterColumnShieldFlatTopCircular(self): + """Makes a firstwall from a CenterColumnShieldFlatTopCircular and + checks the volume is smaller than the shield.""" + + a = paramak.CenterColumnShieldFlatTopCircular( + height=500, + arc_height=300, + inner_radius=30, + mid_radius=70, + outer_radius=120) + b = paramak.InboardFirstwallFCCS( + central_column_shield=a, + thickness=20, + rotation_angle=180) + assert a.solid is not None + assert b.solid is not None + assert a.volume > b.volume + + def test_construction_with_wrong_column_shield_type(self): + def test_construction_with_string(): + """Only CenterColumnShields are acceptable inputs for inputs, this + should fail as it trys to use a string.""" + + b = paramak.InboardFirstwallFCCS( + central_column_shield="incorrect type", + thickness=20, + rotation_angle=180) + b.solid + + self.assertRaises( + ValueError, + test_construction_with_string) + + def test_boolean_union(self): + """Makes two halves of a 360 firstwall and performs a union and checks + that the volume corresponds to 2 times the volume of 1 half.""" + + a = paramak.CenterColumnShieldFlatTopCircular( + height=500, + arc_height=300, + inner_radius=30, + mid_radius=70, + outer_radius=120) + b = paramak.InboardFirstwallFCCS( + central_column_shield=a, + thickness=20, + rotation_angle=180, + azimuth_placement_angle=0) + + c = paramak.InboardFirstwallFCCS( + central_column_shield=a, + thickness=20, + rotation_angle=180, + azimuth_placement_angle=180, + union=b) + assert np.isclose(c.volume, 2 * b.volume) + + def test_azimuth_placement_angle(self): + """Makes two 180 degree firstwalls (one is rotated 90 degree), performs + a cut and checks that the volume corresponds to 0.5 times the volume of + 1 half.""" + + a = paramak.CenterColumnShieldFlatTopCircular( + height=500, + arc_height=300, + inner_radius=30, + mid_radius=70, + outer_radius=120) + b = paramak.InboardFirstwallFCCS( + central_column_shield=a, + thickness=20, + rotation_angle=180, + azimuth_placement_angle=0) + + c = paramak.InboardFirstwallFCCS( + central_column_shield=a, + thickness=20, + rotation_angle=180, + azimuth_placement_angle=90, + cut=b) + assert np.isclose(c.volume, 0.5 * b.volume) + + def test_cut_attribute(self): + """Creates a firstwall then resets its cut attribute and checks that + the shape is not affected.""" + + a = paramak.CenterColumnShieldCylinder( + height=100, + inner_radius=20, + outer_radius=80) + b = paramak.InboardFirstwallFCCS( + central_column_shield=a, + thickness=20, + rotation_angle=180) + + volume_1 = b.volume + b.cut = None + volume_2 = b.volume + assert np.isclose(volume_1, volume_2) diff --git a/tests/test_parametric_components/test_InnerTfCoilsCircular.py b/tests/test_parametric_components/test_InnerTfCoilsCircular.py new file mode 100644 index 000000000..fe23d95f7 --- /dev/null +++ b/tests/test_parametric_components/test_InnerTfCoilsCircular.py @@ -0,0 +1,89 @@ + +import unittest + +import paramak + + +class TestInnerTfCoilsCircular(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.InnerTfCoilsCircular( + height=500, + inner_radius=50, + outer_radius=150, + number_of_coils=6, + gap_size=5 + ) + + def test_default_parameters(self): + """Checks that the default parameters of an InnerTfCoilsCircular are correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.azimuth_start_angle == 0 + assert self.test_shape.stp_filename == "InnerTfCoilsCircular.stp" + assert self.test_shape.stl_filename == "InnerTfCoilsCircular.stl" + assert self.test_shape.material_tag == "inner_tf_coil_mat" + assert self.test_shape.workplane == "XY" + assert self.test_shape.rotation_axis == "Z" + + def test_points_calculation(self): + """Checks that the points used to construct the InnerTFCoilsCircular component + are calculated correctly from the parameters given.""" + + assert self.test_shape.points == [ + (49.937460888595446, 2.5, 'circle'), + (43.300748759659555, 25.000903120744287, 'circle'), + (27.1320420790315, 41.99824154201773, 'straight'), + (77.154447582418, 128.6358861991937, 'circle'), + (129.90375269002172, 75.00010024693078, 'circle'), + (149.97916521970643, 2.5, 'straight'), + (49.937460888595446, 2.5, 'circle') + ] + + def test_creation(self): + """Creates an inner tf coil using the InnerTfCoilsCircular parametric + component and checks that a cadquery solid is created.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + + def test_azimuth_offset(self): + """Creates an inner tf coil using the InnerTfCoilsCircular parametric + component and checks that the azimuthal start angle can be changed + correctly.""" + + assert self.test_shape.azimuth_placement_angle == [ + 0, 60, 120, 180, 240, 300] + self.test_shape.azimuth_start_angle = 20 + assert self.test_shape.azimuth_placement_angle == [ + 20, 80, 140, 200, 260, 320] + + def test_attributes(self): + """Checks that changing the attributes of InnerTfCoilsCircular affects + the cadquery solid produced.""" + + test_volume = self.test_shape.volume + + self.test_shape.height = 1000 + assert test_volume == self.test_shape.volume * 0.5 + self.test_shape.height = 500 + self.test_shape.inner_radius = 30 + assert test_volume < self.test_shape.volume + self.test_shape.inner_radius = 50 + self.test_shape.outer_radius = 170 + assert test_volume < self.test_shape.volume + + def test_gap_size(self): + """Checks that a ValueError is raised when a too large gap_size is + used.""" + + def test_incorrect_gap_size(): + self.test_shape.inner_radius = 20 + self.test_shape.outer_radius = 40 + self.test_shape.gap_size = 50 + self.test_shape.solid + + self.assertRaises( + ValueError, + test_incorrect_gap_size + ) diff --git a/tests/test_parametric_components/test_InnerTfCoilsFlat.py b/tests/test_parametric_components/test_InnerTfCoilsFlat.py new file mode 100644 index 000000000..f6979dfdf --- /dev/null +++ b/tests/test_parametric_components/test_InnerTfCoilsFlat.py @@ -0,0 +1,87 @@ + +import unittest + +import paramak + + +class TestInnerTfCoilsFlat(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.InnerTfCoilsFlat( + height=500, + inner_radius=50, + outer_radius=150, + number_of_coils=6, + gap_size=5 + ) + + def test_default_parameters(self): + """Checks that the default parameters of an InnerTfCoilsFlat are correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.azimuth_start_angle == 0 + assert self.test_shape.stp_filename == "InnerTfCoilsFlat.stp" + assert self.test_shape.stl_filename == "InnerTfCoilsFlat.stl" + assert self.test_shape.material_tag == "inner_tf_coil_mat" + assert self.test_shape.workplane == "XY" + assert self.test_shape.rotation_axis == "Z" + + def test_points_calculation(self): + """Checks that the points used to construct the InnerTfCoilsFlat component are + calculated correctly from the parameters given.""" + + assert self.test_shape.points == [ + (49.937460888595446, 2.5, 'straight'), + (27.1320420790315, 41.99824154201773, 'straight'), + (77.154447582418, 128.6358861991937, 'straight'), + (149.97916521970643, 2.5, 'straight'), + (49.937460888595446, 2.5, 'straight') + ] + + def test_creation(self): + """Creates an inner tf coil using the InnerTFCoilsFlat parametric + component and checks that a cadquery solid is created.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + + def test_azimuth_offset(self): + """Creates an inner tf coil using the InnerTfCoilsFlat parametric + component and checks that the azimuthal start angle can be changed + correctly.""" + + assert self.test_shape.azimuth_placement_angle == [ + 0, 60, 120, 180, 240, 300] + self.test_shape.azimuth_start_angle = 20 + assert self.test_shape.azimuth_placement_angle == [ + 20, 80, 140, 200, 260, 320] + + def test_attributes(self): + """Checks that changing the attributes of InnerTfCoilsFlat affects the + cadquery solid produced.""" + + test_volume = self.test_shape.volume + + self.test_shape.height = 1000 + assert test_volume == self.test_shape.volume * 0.5 + self.test_shape.height = 500 + self.test_shape.inner_radius = 30 + assert test_volume < self.test_shape.volume + self.test_shape.inner_radius = 50 + self.test_shape.outer_radius = 170 + assert test_volume < self.test_shape.volume + + def test_gap_size(self): + """Checks that a ValueError is raised when a too large gap_size is + used.""" + + def test_incorrect_gap_size(): + self.test_shape.inner_radius = 20 + self.test_shape.outer_radius = 40 + self.test_shape.gap_size = 50 + self.test_shape.solid + + self.assertRaises( + ValueError, + test_incorrect_gap_size + ) diff --git a/tests/test_parametric_components/test_Plasma.py b/tests/test_parametric_components/test_Plasma.py new file mode 100644 index 000000000..9771be5e8 --- /dev/null +++ b/tests/test_parametric_components/test_Plasma.py @@ -0,0 +1,234 @@ + +import os +import unittest +from pathlib import Path + +import paramak + + +class TestPlasma(unittest.TestCase): + def test_plasma_attributes(self): + """Creates a plasma object using the Plasma parametric component and + checks that its attributes can be set correctly.""" + + test_plasma = paramak.Plasma() + + assert isinstance(test_plasma.elongation, float) + + def test_plasma_elongation_min_setting(): + """checks ValueError is raised when an elongation < 0 is specified""" + + test_plasma.elongation = -1 + + self.assertRaises(ValueError, test_plasma_elongation_min_setting) + + def test_plasma_elongation_max_setting(): + """checks ValueError is raised when an elongation > 4 is specified""" + + test_plasma.elongation = 400 + + self.assertRaises(ValueError, test_plasma_elongation_max_setting) + + def minor_radius_out_of_range(): + """checks ValueError is raised when an minor_radius < 1 is + specified""" + + test_plasma.minor_radius = 0.5 + + self.assertRaises(ValueError, minor_radius_out_of_range) + + def major_radius_out_of_range(): + """checks ValueError is raised when an manor_radius < 1 is + specified""" + + test_plasma.major_radius = 0.5 + + self.assertRaises(ValueError, major_radius_out_of_range) + + def test_plasma_points_of_interest(self): + test_plasma = paramak.Plasma(vertical_displacement=2) + assert test_plasma.high_point == ( + test_plasma.major_radius - + test_plasma.triangularity * test_plasma.minor_radius, + test_plasma.elongation * test_plasma.minor_radius + + test_plasma.vertical_displacement, + ) + assert test_plasma.low_point == ( + test_plasma.major_radius - + test_plasma.triangularity * test_plasma.minor_radius, + -test_plasma.elongation * test_plasma.minor_radius + + test_plasma.vertical_displacement, + ) + assert test_plasma.outer_equatorial_point == ( + test_plasma.major_radius + test_plasma.minor_radius, + test_plasma.vertical_displacement + ) + assert test_plasma.inner_equatorial_point == ( + test_plasma.major_radius - test_plasma.minor_radius, + test_plasma.vertical_displacement + ) + + def test_plasma_x_points(self): + """Creates several plasmas with different configurations using the + Plasma parametric component and checks the location of the x point for + each.""" + + for ( + triangularity, + elongation, + minor_radius, + major_radius, + vertical_displacement, + ) in zip( + [-0.7, 0, 0.5], # triangularity + [1, 1.5, 2], # elongation + [100, 200, 300], # minor radius + [300, 400, 600], # major radius + [0, -10, 5], + ): # displacement + + for config in ["non-null", "single-null", "double-null"]: + + # Run + test_plasma = paramak.Plasma( + configuration=config, + triangularity=triangularity, + elongation=elongation, + minor_radius=minor_radius, + major_radius=major_radius, + vertical_displacement=vertical_displacement, + ) + + # Expected + expected_lower_x_point, expected_upper_x_point = None, None + if config == "single-null" or config == "double-null": + expected_lower_x_point = (1 - + (1 + + test_plasma.x_point_shift) * + triangularity * + minor_radius, - + (1 + + test_plasma.x_point_shift) * + elongation * + minor_radius + + vertical_displacement, ) + + if config == "double-null": + expected_upper_x_point = ( + expected_lower_x_point[0], + (1 + + test_plasma.x_point_shift) * + elongation * + minor_radius + + vertical_displacement, + ) + + # Check + for point, expected_point in zip( + [test_plasma.lower_x_point, test_plasma.upper_x_point], + [expected_lower_x_point, expected_upper_x_point], + ): + assert point == expected_point + + def test_plasma_x_points_plasmaboundaries(self): + """Creates several plasmas with different configurations using the + PlasmaBoundaries parametric component and checks the location of the x + point for each.""" + + for A, triangularity, elongation, minor_radius, major_radius in zip( + [0, 0.05, 0.05], # A + [-0.7, 0, 0.5], # triangularity + [1, 1.5, 2], # elongation + [100, 200, 300], # minor radius + [300, 400, 600], + ): # major radius + + for config in ["non-null", "single-null", "double-null"]: + + # Run + test_plasma = paramak.PlasmaBoundaries( + configuration=config, + A=A, + triangularity=triangularity, + elongation=elongation, + minor_radius=minor_radius, + major_radius=major_radius, + ) + + # Expected + expected_lower_x_point, expected_upper_x_point = None, None + if config == "single-null" or config == "double-null": + expected_lower_x_point = (1 - + (1 + + test_plasma.x_point_shift) * + triangularity * + minor_radius, - + (1 + + test_plasma.x_point_shift) * + elongation * + minor_radius, ) + + if config == "double-null": + expected_upper_x_point = ( + expected_lower_x_point[0], + -expected_lower_x_point[1], + ) + + # Check + for point, expected_point in zip( + [test_plasma.lower_x_point, test_plasma.upper_x_point], + [expected_lower_x_point, expected_upper_x_point], + ): + assert point == expected_point + + def test_plasmaboundaries_solid(self): + """Create a default PlasmaBoundaries shape and check a solid can be + created""" + test_plasma = paramak.PlasmaBoundaries() + for config in ["non-null", "single-null", "double-null"]: + test_plasma.configuration = config + assert test_plasma.solid is not None + + def test_export_plasma_source(self): + """Creates a plasma using the Plasma parametric component and checks a + stp file of the shape can be exported using the export_stp method.""" + + test_plasma = paramak.Plasma() + + os.system("rm plasma.stp") + + test_plasma.export_stp("plasma.stp") + + assert Path("plasma.stp").exists() + os.system("rm plasma.stp") + + def test_export_plasma_from_points_export(self): + """Creates a plasma using the PlasmaFromPoints parametric component + and checks a stp file of the shape can be exported using the export_stp + method.""" + + test_plasma = paramak.PlasmaFromPoints( + outer_equatorial_x_point=500, + inner_equatorial_x_point=300, + high_point=(400, 200), + rotation_angle=180, + ) + + os.system("rm plasma.stp") + + test_plasma.export_stp("plasma.stp") + assert test_plasma.high_point[0] > test_plasma.inner_equatorial_x_point + assert test_plasma.high_point[0] < test_plasma.outer_equatorial_x_point + assert test_plasma.outer_equatorial_x_point > test_plasma.inner_equatorial_x_point + assert Path("plasma.stp").exists() + os.system("rm plasma.stp") + + # TODO: fix issue #435 + # def test_plasma_relative_volume(self): + # """Creates plasmas using the Plasma parametric component and checks that + # the relative volumes of the solids created are correct""" + + # test_plasma = paramak.Plasma() + # test_plasma_volume = test_plasma.volume + # test_plasma.rotation_angle = 180 + # assert test_plasma.volume == pytest.approx(test_plasma_volume * 0.5) diff --git a/tests/test_parametric_components/test_PoloidalFieldCoil.py b/tests/test_parametric_components/test_PoloidalFieldCoil.py new file mode 100644 index 000000000..4d8b9d35f --- /dev/null +++ b/tests/test_parametric_components/test_PoloidalFieldCoil.py @@ -0,0 +1,60 @@ + +import math +import unittest + +import paramak +import pytest + + +class TestPoloidalFieldCoil(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.PoloidalFieldCoil( + height=50, width=60, center_point=(1000, 500) + ) + + def test_default_parameters(self): + """Checks that the default parameters of a PoloidalFieldCoil are correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.stp_filename == "PoloidalFieldCoil.stp" + assert self.test_shape.stl_filename == "PoloidalFieldCoil.stl" + # assert self.test_shape.name == "pf_coil" + assert self.test_shape.material_tag == "pf_coil_mat" + + def test_points_calculation(self): + """Checks that the points used to construct the PoloidalFieldCoil are calculated + correctly from the parameters given.""" + + assert self.test_shape.points == [ + (1030.0, 525.0, 'straight'), (1030.0, 475.0, 'straight'), + (970.0, 475.0, 'straight'), (970.0, 525.0, 'straight'), + (1030.0, 525.0, 'straight') + ] + + def test_creation(self): + """Creates a pf coil using the PoloidalFieldCoil parametric component + and checks that a cadquery solid is created.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + + def test_absolute_volume(self): + """Creates a pf coil using the PoloidalFieldCoil parametric component + and checks that the volume is correct""" + + assert self.test_shape.volume == pytest.approx( + 50 * 60 * math.pi * 2 * 1000) + + def test_absolute_areas(self): + """Creates a pf coil using the PoloidalFieldCoil parametric component + and checks that the areas are correct""" + + assert len(self.test_shape.areas) == 4 + assert len(set([round(i) for i in self.test_shape.areas])) == 3 + assert self.test_shape.areas.count( + pytest.approx(60 * math.pi * 2 * 1000)) == 2 + assert self.test_shape.areas.count( + pytest.approx(50 * math.pi * 2 * 970)) == 1 + assert self.test_shape.areas.count( + pytest.approx(50 * math.pi * 2 * 1030)) == 1 diff --git a/tests/test_parametric_components/test_PoloidalFieldCoilCase.py b/tests/test_parametric_components/test_PoloidalFieldCoilCase.py new file mode 100644 index 000000000..35eba51da --- /dev/null +++ b/tests/test_parametric_components/test_PoloidalFieldCoilCase.py @@ -0,0 +1,70 @@ + +import math +import unittest + +import paramak +import pytest + + +class TestPoloidalFieldCoilCase(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.PoloidalFieldCoilCase( + casing_thickness=5, + coil_height=50, + coil_width=50, + center_point=(1000, 500) + ) + + def test_default_parameters(self): + """Checks that the default parameters of a PoloidalFieldCoilCase are correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.stp_filename == "PoloidalFieldCoilCase.stp" + assert self.test_shape.stl_filename == "PoloidalFieldCoilCase.stl" + assert self.test_shape.material_tag == "pf_coil_case_mat" + + def test_points_calculation(self): + """Checks that the points used to construct the InnerTfCoilsFlat component are + calculated correctly from the parameters given.""" + + assert self.test_shape.points == [(1025.0, 525.0, 'straight'), + (1025.0, 475.0, 'straight'), (975.0, 475.0, 'straight'), + (975.0, 525.0, 'straight'), (1025.0, 525.0, 'straight'), + (1030.0, 530.0, 'straight'), (1030.0, 470.0, 'straight'), + (970.0, 470.0, 'straight'), (970.0, 530.0, 'straight'), + (1030.0, 530.0, 'straight'), (1025.0, 525.0, 'straight') + ] + + def test_creation(self): + """Creates a pf coil case using the PoloidalFieldCoilCase parametric + component and checks that a cadquery solid is created.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + + def test_absolute_volume(self): + """Creates a pf coil case using the PoloidalFieldCoilCase parametric + component and checks that its volume is correct.""" + + assert self.test_shape.volume == pytest.approx( + (math.pi * 2 * 1000) * ((60 * 5 * 2) + (50 * 5 * 2))) + + def test_absolute_areas(self): + """Creates a pf coil case using the PoloidalFieldCoilCase parametric + component and checks that the areas of its faces are correct.""" + + assert len(self.test_shape.areas) == 8 + assert len(set([round(i) for i in self.test_shape.areas])) == 6 + assert self.test_shape.areas.count( + pytest.approx(50 * math.pi * 2 * 1000)) == 2 + assert self.test_shape.areas.count( + pytest.approx(60 * math.pi * 2 * 1000)) == 2 + assert self.test_shape.areas.count( + pytest.approx(50 * math.pi * 2 * 1025)) == 1 + assert self.test_shape.areas.count( + pytest.approx(50 * math.pi * 2 * 975)) == 1 + assert self.test_shape.areas.count( + pytest.approx(60 * math.pi * 2 * 1030)) == 1 + assert self.test_shape.areas.count( + pytest.approx(60 * math.pi * 2 * 970)) == 1 diff --git a/tests/test_parametric_components/test_PoloidalFieldCoilCaseFC.py b/tests/test_parametric_components/test_PoloidalFieldCoilCaseFC.py new file mode 100644 index 000000000..fa7bac0af --- /dev/null +++ b/tests/test_parametric_components/test_PoloidalFieldCoilCaseFC.py @@ -0,0 +1,59 @@ + +import math +import unittest + +import paramak +import pytest + + +class TestPoloidalFieldCoilCaseFC(unittest.TestCase): + + def setUp(self): + self.pf_coil = paramak.PoloidalFieldCoil( + height=50, width=60, center_point=(1000, 500) + ) + + self.test_shape = paramak.PoloidalFieldCoilCaseFC( + pf_coil=self.pf_coil, casing_thickness=5 + ) + + def test_default_parameters(self): + """Checks that the default parameters of a PoloidalFieldCoilCaseFC are correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.stp_filename == "PoloidalFieldCoilCaseFC.stp" + assert self.test_shape.stl_filename == "PoloidalFieldCoilCaseFC.stl" + assert self.test_shape.material_tag == "pf_coil_case_mat" + + def test_creation(self): + """Creates a pf coil case using the PoloidalFieldCoilCaseFC parametric + component and checks that a cadquery solid is created.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + + def test_absolute_volume(self): + """Creates a pf coil case using the PoloidalFieldCoilCaseFC parametric + component and checks that its volume is correct.""" + + assert self.test_shape.volume == pytest.approx( + (math.pi * 2 * 1000) * ((50 * 5 * 2) + (70 * 5 * 2))) + + def test_absolute_areas(self): + """Creates a pf coil case using the PoloidalFieldCoilCaseFC parametric + component and checks that the areas of its faces are correct""" + + assert len(self.test_shape.areas) == 8 + assert len(set([round(i) for i in self.test_shape.areas])) == 6 + assert self.test_shape.areas.count( + pytest.approx(60 * math.pi * 2 * 1000)) == 2 + assert self.test_shape.areas.count( + pytest.approx(70 * math.pi * 2 * 1000)) == 2 + assert self.test_shape.areas.count( + pytest.approx(50 * math.pi * 2 * 1030)) == 1 + assert self.test_shape.areas.count( + pytest.approx(50 * math.pi * 2 * 970)) == 1 + assert self.test_shape.areas.count( + pytest.approx(60 * math.pi * 2 * 1035)) == 1 + assert self.test_shape.areas.count( + pytest.approx(60 * math.pi * 2 * 965)) == 1 diff --git a/tests/test_parametric_components/test_PoloidalFieldCoilCaseSet.py b/tests/test_parametric_components/test_PoloidalFieldCoilCaseSet.py new file mode 100644 index 000000000..7a2b1d638 --- /dev/null +++ b/tests/test_parametric_components/test_PoloidalFieldCoilCaseSet.py @@ -0,0 +1,133 @@ + +import math +import unittest + +import paramak +import pytest + + +class TestPoloidalFieldCoilCaseSet(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.PoloidalFieldCoilCaseSet( + heights=[10, 10, 20, 20], + widths=[10, 10, 20, 40], + center_points=[(100, 100), (100, 150), (50, 200), (50, 50)], + casing_thicknesses=[5, 10, 5, 10], + ) + + def test_default_parameters(self): + """Checks that the default parameters of a PoloidalFieldCoilCaseSet are correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.stp_filename == "PoloidalFieldCoilCaseSet.stp" + assert self.test_shape.stl_filename == "PoloidalFieldCoilCaseSet.stl" + # assert self.test_shape.name == "pf_coil_case_set" + assert self.test_shape.material_tag == "pf_coil_case_mat" + + def test_points_calculation(self): + """Checks that the points used to construct the PoloidalFieldCoilCaseSetFC are + calculated correctly from the parameters given.""" + + assert self.test_shape.points == [ + (105.0, 105.0, 'straight'), (105.0, 95.0, 'straight'), (95.0, 95.0, 'straight'), + (95.0, 105.0, 'straight'), (105.0, 105.0, 'straight'), (110.0, 110.0, 'straight'), + (110.0, 90.0, 'straight'), (90.0, 90.0, 'straight'), (90.0, 110.0, 'straight'), + (110.0, 110.0, 'straight'), (105.0, 155.0, 'straight'), (105.0, 145.0, 'straight'), + (95.0, 145.0, 'straight'), (95.0, 155.0, 'straight'), (105.0, 155.0, 'straight'), + (115.0, 165.0, 'straight'), (115.0, 135.0, 'straight'), (85.0, 135.0, 'straight'), + (85.0, 165.0, 'straight'), (115.0, 165.0, 'straight'), (60.0, 210.0, 'straight'), + (60.0, 190.0, 'straight'), (40.0, 190.0, 'straight'), (40.0, 210.0, 'straight'), + (60.0, 210.0, 'straight'), (65.0, 215.0, 'straight'), (65.0, 185.0, 'straight'), + (35.0, 185.0, 'straight'), (35.0, 215.0, 'straight'), (65.0, 215.0, 'straight'), + (70.0, 60.0, 'straight'), (70.0, 40.0, 'straight'), (30.0, 40.0, 'straight'), + (30.0, 60.0, 'straight'), (70.0, 60.0, 'straight'), (80.0, 70.0, 'straight'), + (80.0, 30.0, 'straight'), (20.0, 30.0, 'straight'), (20.0, 70.0, 'straight'), + (80.0, 70.0, 'straight'), (105.0, 105.0, 'straight') + ] + + def test_creation(self): + """Creates a set of pf coils using the PoloidalFieldCoilCaseSet + parametric component and passing all required args, and checks + that a solid with the correct number of solids is created.""" + + assert self.test_shape.solid is not None + assert len(self.test_shape.solid.Solids()) == 4 + + def test_creation_with_zero_thickness(self): + """Creates a set of pf coils using the PoloidalFieldCoilCaseSet + parametric component and passing a 0 entry into the casing_thicknesses + list, and checks that a solid with the correct number of solids is + created.""" + + self.test_shape.casing_thicknesses = [5, 0, 10, 10] + + assert self.test_shape.solid is not None + assert len(self.test_shape.solid.Solids()) == 3 + + def test_absolute_volume(self): + """Creates a set of pf coils using the PoloidalFieldCoilCaseSet + parametric component and checks that the volume is correct.""" + + assert self.test_shape.volume == pytest.approx((((20 * 5 * 2) + + (10 * 5 * 2)) * math.pi * 2 * 100) + (((30 * 10 * 2) + + (10 * 10 * 2)) * math.pi * 2 * 100) + (((30 * 5 * 2) + + (20 * 5 * 2)) * math.pi * 2 * 50) + (((60 * 10 * 2) + + (20 * 10 * 2)) * math.pi * 2 * 50)) + + def test_absolute_areas(self): + """Creates a set of pf coils using the PoloidalFieldCoilCaseSet + parametric component and checks that the areas are correct""" + + assert len(self.test_shape.areas) == 32 + assert len(set([round(i) for i in self.test_shape.areas])) == 16 + assert self.test_shape.areas.count( + pytest.approx(10 * math.pi * 2 * 100)) == 6 + assert self.test_shape.areas.count( + pytest.approx(40 * math.pi * 2 * 50)) == 4 + assert self.test_shape.areas.count( + pytest.approx(30 * math.pi * 2 * 100)) == 4 + assert self.test_shape.areas.count( + pytest.approx(30 * math.pi * 2 * 50)) == 2 + assert self.test_shape.areas.count( + pytest.approx(10 * math.pi * 2 * 105)) == 3 + assert self.test_shape.areas.count( + pytest.approx(10 * math.pi * 2 * 95)) == 2 + assert self.test_shape.areas.count( + pytest.approx(20 * math.pi * 2 * 110)) == 1 + assert self.test_shape.areas.count( + pytest.approx(20 * math.pi * 2 * 90)) == 1 + assert self.test_shape.areas.count( + pytest.approx(30 * math.pi * 2 * 115)) == 1 + assert self.test_shape.areas.count( + pytest.approx(30 * math.pi * 2 * 85)) == 1 + assert self.test_shape.areas.count( + pytest.approx(20 * math.pi * 2 * 60)) == 1 + assert self.test_shape.areas.count( + pytest.approx(20 * math.pi * 2 * 40)) == 2 + assert self.test_shape.areas.count( + pytest.approx(30 * math.pi * 2 * 65)) == 1 + assert self.test_shape.areas.count( + pytest.approx(20 * math.pi * 2 * 70)) == 1 + assert self.test_shape.areas.count( + pytest.approx(20 * math.pi * 2 * 30)) == 1 + assert self.test_shape.areas.count( + pytest.approx(40 * math.pi * 2 * 80)) == 1 + + def test_invalid_args(self): + """Creates PoloidalFieldCoilCaseSets with invalid arguments and checks + that the correct errors are raised.""" + + def test_invalid_casing_thicknesses_1(): + self.test_shape.casing_thicknesses = [5, 5, 10] + self.test_shape.solid + + def test_invalid_casing_thicknesses_2(): + self.test_shape.casing_thicknesses = [5, 5, 5, 'ten'] + + def test_invalid_casing_thicknesses_3(): + self.test_shape.casing_thicknesses = "ten" + + self.assertRaises(ValueError, test_invalid_casing_thicknesses_1) + self.assertRaises(ValueError, test_invalid_casing_thicknesses_2) + self.assertRaises(ValueError, test_invalid_casing_thicknesses_3) diff --git a/tests/test_parametric_components/test_PoloidalFieldCoilCaseSetFC.py b/tests/test_parametric_components/test_PoloidalFieldCoilCaseSetFC.py new file mode 100644 index 000000000..a5f9d4a51 --- /dev/null +++ b/tests/test_parametric_components/test_PoloidalFieldCoilCaseSetFC.py @@ -0,0 +1,213 @@ + +import math +import unittest + +import paramak +import pytest + + +class TestPoloidalFieldCoilCaseSetFC(unittest.TestCase): + + def setUp(self): + self.pf_coils_set = paramak.PoloidalFieldCoilSet( + heights=[10, 10, 20, 20], + widths=[10, 10, 20, 40], + center_points=[(100, 100), (100, 150), (50, 200), (50, 50)], + ) + + self.test_shape = paramak.PoloidalFieldCoilCaseSetFC( + pf_coils=self.pf_coils_set, + casing_thicknesses=[5, 10, 5, 10], + ) + + def test_default_parameters(self): + """Checks that the default parameters of a PoloidalFieldCoilCaseSetFC are correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.stp_filename == "PoloidalFieldCoilCaseSetFC.stp" + assert self.test_shape.stl_filename == "PoloidalFieldCoilCaseSetFC.stl" + # assert self.test_shape.name == "pf_coil_case_set_fc" + assert self.test_shape.material_tag == "pf_coil_mat" + + def test_points_calculation(self): + """Checks that the points used to construct the PoloidalFieldCoilCaseSetFC are + calculated correctly from the parameters given.""" + + assert self.test_shape.points == [ + (105.0, 105.0, 'straight'), (105.0, 95.0, 'straight'), (95.0, 95.0, 'straight'), + (95.0, 105.0, 'straight'), (105.0, 105.0, 'straight'), (110.0, 110.0, 'straight'), + (110.0, 90.0, 'straight'), (90.0, 90.0, 'straight'), (90.0, 110.0, 'straight'), + (110.0, 110.0, 'straight'), (105.0, 155.0, 'straight'), (105.0, 145.0, 'straight'), + (95.0, 145.0, 'straight'), (95.0, 155.0, 'straight'), (105.0, 155.0, 'straight'), + (115.0, 165.0, 'straight'), (115.0, 135.0, 'straight'), (85.0, 135.0, 'straight'), + (85.0, 165.0, 'straight'), (115.0, 165.0, 'straight'), (60.0, 210.0, 'straight'), + (60.0, 190.0, 'straight'), (40.0, 190.0, 'straight'), (40.0, 210.0, 'straight'), + (60.0, 210.0, 'straight'), (65.0, 215.0, 'straight'), (65.0, 185.0, 'straight'), + (35.0, 185.0, 'straight'), (35.0, 215.0, 'straight'), (65.0, 215.0, 'straight'), + (70.0, 60.0, 'straight'), (70.0, 40.0, 'straight'), (30.0, 40.0, 'straight'), + (30.0, 60.0, 'straight'), (70.0, 60.0, 'straight'), (80.0, 70.0, 'straight'), + (80.0, 30.0, 'straight'), (20.0, 30.0, 'straight'), (20.0, 70.0, 'straight'), + (80.0, 70.0, 'straight'), (105.0, 105.0, 'straight') + ] + + def test_from_pf_coil_set(self): + """Checks that a set of PF coil cases can be constructed from a PF coils object + using the PoloidalField~CoilCaseSetFC parametric shape.""" + + assert self.test_shape.solid is not None + assert len(self.test_shape.solid.Solids()) == 4 + assert len(self.pf_coils_set.solid.Solids()) == 4 + + def test_with_zero_thickness(self): + """Creates a set of PF coil cases from a PF coils object and sets one + of the casing thicknesses to 0.""" + + self.test_shape.casing_thicknesses = [5, 5, 0, 10] + + assert self.test_shape.solid is not None + assert len(self.test_shape.solid.Solids()) == 3 + assert len(self.pf_coils_set.solid.Solids()) == 4 + + def test_from_pf_coil_set_absolute_volume(self): + """Creates a set of pf coil cases from a pf coil set object and checks + that the volume is correct.""" + + assert self.test_shape.volume == pytest.approx((((20 * 5 * 2) + (10 * 5 * 2)) * math.pi * 2 * 100) + (((30 * 10 * 2) + ( + 10 * 10 * 2)) * math.pi * 2 * 100) + (((30 * 5 * 2) + (20 * 5 * 2)) * math.pi * 2 * 50) + (((60 * 10 * 2) + (20 * 10 * 2)) * math.pi * 2 * 50)) + + def test_from_pf_coil_set_absolute_areas(self): + """Creates a set of pf coil cases from a pf coil set object and checks + that the areas are correct""" + + assert len(self.test_shape.areas) == 32 + assert len(set([round(i) for i in self.test_shape.areas])) == 16 + assert self.test_shape.areas.count( + pytest.approx(10 * math.pi * 2 * 100)) == 6 + assert self.test_shape.areas.count( + pytest.approx(40 * math.pi * 2 * 50)) == 4 + assert self.test_shape.areas.count( + pytest.approx(30 * math.pi * 2 * 100)) == 4 + assert self.test_shape.areas.count( + pytest.approx(30 * math.pi * 2 * 50)) == 2 + assert self.test_shape.areas.count( + pytest.approx(10 * math.pi * 2 * 105)) == 3 + assert self.test_shape.areas.count( + pytest.approx(10 * math.pi * 2 * 95)) == 2 + assert self.test_shape.areas.count( + pytest.approx(20 * math.pi * 2 * 110)) == 1 + assert self.test_shape.areas.count( + pytest.approx(20 * math.pi * 2 * 90)) == 1 + assert self.test_shape.areas.count( + pytest.approx(30 * math.pi * 2 * 115)) == 1 + assert self.test_shape.areas.count( + pytest.approx(30 * math.pi * 2 * 85)) == 1 + assert self.test_shape.areas.count( + pytest.approx(20 * math.pi * 2 * 60)) == 1 + assert self.test_shape.areas.count( + pytest.approx(20 * math.pi * 2 * 40)) == 2 + assert self.test_shape.areas.count( + pytest.approx(30 * math.pi * 2 * 65)) == 1 + assert self.test_shape.areas.count( + pytest.approx(20 * math.pi * 2 * 70)) == 1 + assert self.test_shape.areas.count( + pytest.approx(20 * math.pi * 2 * 30)) == 1 + assert self.test_shape.areas.count( + pytest.approx(40 * math.pi * 2 * 80)) == 1 + + def test_incorrect_args(self): + """Creates a solid using the PoloidalFieldCoilCaseSet with incorrect + args""" + + def test_PoloidalFieldCoilSet_incorrect_lengths_FC(): + """Checks PoloidalFieldCoilSet with the wrong number of casing + thicknesses (3) using a coil set object with 4 pf_coils.""" + + self.test_shape.casing_thicknesses = [5, 5, 10] + self.test_shape.solid + + self.assertRaises( + ValueError, + test_PoloidalFieldCoilSet_incorrect_lengths_FC) + + def test_PoloidalFieldCoilSet_incorrect_lengths(): + """Checks PoloidalFieldCoilSet with the wrong number of casing + thicknesses using a list.""" + + self.pf_coils_set.height = 10 + self.pf_coils_set.width = 10 + self.pf_coils_set.center_point = (100, 100) + + self.test_shape.pf_coils = [self.pf_coils_set] + self.test_shape.solid + + self.assertRaises( + ValueError, + test_PoloidalFieldCoilSet_incorrect_lengths) + + def test_PoloidalFieldCoilSet_incorrect_pf_coil(): + """Checks PoloidalFieldCoilSet with the pf_coils as an incorrect + entry.""" + + self.test_shape.pf_coils = 20 + self.test_shape.solid + + self.assertRaises( + ValueError, + test_PoloidalFieldCoilSet_incorrect_pf_coil) + + def test_from_list(self): + """Creates a set of PF coil cases from a list of PF coils with a list + of thicknesses.""" + + pf_coils_1 = paramak.PoloidalFieldCoil(height=10, + width=10, + center_point=(100, 100)) + + pf_coils_2 = paramak.PoloidalFieldCoil(height=10, + width=10, + center_point=(100, 150)) + + pf_coils_3 = paramak.PoloidalFieldCoil(height=20, + width=20, + center_point=(50, 200)) + + pf_coils_4 = paramak.PoloidalFieldCoil(height=20, + width=40, + center_point=(50, 50)) + + test_shape = paramak.PoloidalFieldCoilCaseSetFC( + pf_coils=[pf_coils_1, pf_coils_2, pf_coils_3, pf_coils_4], + casing_thicknesses=[5, 5, 10, 10] + ) + + assert test_shape.solid is not None + assert len(test_shape.solid.Solids()) == 4 + + def test_PoloidalFieldCoilCaseFC_with_number_thickness(self): + """Creates a set of PF coil cases from a list of PF coils with a + single numerical thicknesses.""" + + pf_coils_1 = paramak.PoloidalFieldCoil(height=10, + width=10, + center_point=(100, 100)) + + pf_coils_2 = paramak.PoloidalFieldCoil(height=10, + width=10, + center_point=(100, 150)) + + pf_coils_3 = paramak.PoloidalFieldCoil(height=20, + width=20, + center_point=(50, 200)) + + pf_coils_4 = paramak.PoloidalFieldCoil(height=20, + width=40, + center_point=(50, 50)) + + test_shape = paramak.PoloidalFieldCoilCaseSetFC( + pf_coils=[pf_coils_1, pf_coils_2, pf_coils_3, pf_coils_4], + casing_thicknesses=10, + ) + + assert test_shape.casing_thicknesses == 10 + assert test_shape.solid is not None + assert len(test_shape.solid.Solids()) == 4 diff --git a/tests/test_parametric_components/test_PoloidalFieldCoilFP.py b/tests/test_parametric_components/test_PoloidalFieldCoilFP.py new file mode 100644 index 000000000..ac30117a7 --- /dev/null +++ b/tests/test_parametric_components/test_PoloidalFieldCoilFP.py @@ -0,0 +1,18 @@ + +import unittest + +import paramak +import pytest + + +class TestPoloidalFieldCoilFP(unittest.TestCase): + + def test_shape_construction_and_volume(self): + """Cuts a vessel cylinder with several different size port cutters.""" + + test_component = paramak.PoloidalFieldCoilFP( + corner_points=[(10, 10), (20, 22)]) + + assert test_component.volume == pytest.approx(12063.715789784808) + assert test_component.corner_points == [(10, 10), (20, 22)] + assert test_component.solid is not None diff --git a/tests/test_parametric_components/test_PoloidalFieldCoilSet.py b/tests/test_parametric_components/test_PoloidalFieldCoilSet.py new file mode 100644 index 000000000..1e03f0486 --- /dev/null +++ b/tests/test_parametric_components/test_PoloidalFieldCoilSet.py @@ -0,0 +1,128 @@ + +import math +import unittest + +import paramak +import pytest + + +class TestPoloidalFieldCoilSet(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.PoloidalFieldCoilSet( + heights=[10, 15, 5], + widths=[20, 25, 30], + center_points=[(100, 100), (200, 200), (300, 300)] + ) + + def test_default_parameters(self): + """Checks that the default parameters of a PoloidalFieldCoilSet are correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.stp_filename == "PoloidalFieldCoilSet.stp" + assert self.test_shape.stl_filename == "PoloidalFieldCoilSet.stl" + # assert self.test_shape.name == "pf_coil" + assert self.test_shape.material_tag == "pf_coil_mat" + + def test_points_calculation(self): + """Checks that the points used to construct the PoloidalFieldCoilSet are + calculated correctly from the parameters given.""" + + assert self.test_shape.points == [ + (110.0, 105.0, 'straight'), (110.0, 95.0, 'straight'), (90.0, 95.0, 'straight'), + (90.0, 105.0, 'straight'), (212.5, 207.5, 'straight'), (212.5, 192.5, 'straight'), + (187.5, 192.5, 'straight'), (187.5, 207.5, 'straight'), (315.0, 302.5, 'straight'), + (315.0, 297.5, 'straight'), (285.0, 297.5, 'straight'), (285.0, 302.5, 'straight'), + (110.0, 105.0, 'straight') + ] + + def test_creation(self): + """Creates a solid using the PoloidalFieldCoilSet parametric component + and checks that a cadquery solid is created.""" + + assert self.test_shape.solid is not None + assert len(self.test_shape.solid.Solids()) == 3 + + def test_absolute_volume(self): + """Creates a set of pf coils using the PoloidalFieldCoilSet parametric + component and checks that the volume is correct.""" + + assert self.test_shape.volume == (pytest.approx((10 * 20 * math.pi * (2 * 100)) + ( + 15 * 25 * math.pi * (2 * 200)) + (5 * 30 * math.pi * (2 * 300)))) + + def test_absolute_areas(self): + """Creates a set of pf coils using the PoloidalFieldCoilSet parametric + component and checks that the areas of its faces are correct.""" + + assert len(self.test_shape.areas) == 12 + assert len(set(round(i) for i in self.test_shape.areas)) == 9 + assert self.test_shape.areas.count( + pytest.approx(20 * math.pi * (2 * 100))) == 2 + assert self.test_shape.areas.count( + pytest.approx(25 * math.pi * (2 * 200))) == 2 + assert self.test_shape.areas.count( + pytest.approx(30 * math.pi * (2 * 300))) == 2 + assert self.test_shape.areas.count( + pytest.approx(10 * math.pi * (2 * 90))) == 1 + assert self.test_shape.areas.count( + pytest.approx(10 * math.pi * (2 * 110))) == 1 + assert self.test_shape.areas.count( + pytest.approx(15 * math.pi * (2 * 187.5))) == 1 + assert self.test_shape.areas.count( + pytest.approx(15 * math.pi * (2 * 212.5))) == 1 + assert self.test_shape.areas.count( + pytest.approx(5 * math.pi * (2 * 285))) == 1 + assert self.test_shape.areas.count( + pytest.approx(5 * math.pi * (2 * 315))) == 1 + + def test_incorrect_args(self): + """Creates a solid using the PoloidalFieldCoilSet parametric component + and checks that a cadquery solid is created.""" + + def test_incorrect_height(): + """Checks PoloidalFieldCoilSet with height as the wrong type.""" + + paramak.PoloidalFieldCoilSet( + heights=10, + widths=[20, 20, 20], + center_points=[(100, 100), (200, 200), (300, 300)]) + + self.assertRaises( + ValueError, + test_incorrect_height) + + def test_incorrect_width(): + """Checks PoloidalFieldCoilSet with width as the wrong type.""" + + paramak.PoloidalFieldCoilSet( + heights=[10, 10, 10], + widths=20, + center_points=[(100, 100), (200, 200), (300, 300)]) + + self.assertRaises( + ValueError, + test_incorrect_width) + + def test_incorrect_center_points(): + """Checks PoloidalFieldCoilSet with center_points as the wrong + type.""" + + paramak.PoloidalFieldCoilSet( + heights=[10, 10, 10], + widths=[20, 20, 20], + center_points=100) + + self.assertRaises( + ValueError, + test_incorrect_center_points) + + def test_incorrect_width_length(): + """Checks PoloidalFieldCoilSet with not enough entries in width.""" + paramak.PoloidalFieldCoilSet( + heights=[10, 10, 10], + widths=[20, 20], + center_points=[(100, 100), (200, 200), (300, 300)]) + + self.assertRaises( + ValueError, + test_incorrect_width_length) diff --git a/tests/test_parametric_components/test_PoloidalSegments.py b/tests/test_parametric_components/test_PoloidalSegments.py new file mode 100644 index 000000000..4ed387922 --- /dev/null +++ b/tests/test_parametric_components/test_PoloidalSegments.py @@ -0,0 +1,98 @@ + +import unittest + +import paramak + + +class TestPoloidalSegments(unittest.TestCase): + + def test_solid_count_with_incorect_input(self): + """Checks the segmenter does not take a float as an input.""" + + def create_shape(): + test_shape_to_segment = paramak.PoloidalFieldCoil( + height=100, + width=100, + center_point=(500, 500) + ) + + paramak.PoloidalSegments( + shape_to_segment=test_shape_to_segment, + center_point=(500, 500), + number_of_segments=22.5, + ) + + self.assertRaises( + ValueError, create_shape) + + def test_solid_count_with_incorect_inputs2(self): + """Checks the segmenter does not take a negative int as an input.""" + + def create_shape(): + test_shape_to_segment = paramak.PoloidalFieldCoil( + height=100, + width=100, + center_point=(500, 500) + ) + + paramak.PoloidalSegments( + shape_to_segment=test_shape_to_segment, + center_point=(500, 500), + number_of_segments=-5, + ) + + self.assertRaises( + ValueError, create_shape) + + def test_solid_count(self): + """Creates a rotated hollow ring and segments it into poloidal + sections.""" + + pf_coil = paramak.PoloidalFieldCoil( + height=100, + width=100, + center_point=(500, 500) + ) + + test_shape_to_segment = paramak.PoloidalFieldCoilCaseFC( + pf_coil=pf_coil, + casing_thickness=100 + ) + + test_shape = paramak.PoloidalSegments( + shape_to_segment=test_shape_to_segment, + center_point=(500, 500), + number_of_segments=22, + ) + + assert test_shape.solid is not None + assert len(test_shape.solid.Solids()) == 22 + + def test_solid_count2(self): + """Creates a rotated ring and segments it into poloidal sections.""" + + test_shape_to_segment = paramak.PoloidalFieldCoil( + height=100, + width=100, + center_point=(500, 500) + ) + + test_shape = paramak.PoloidalSegments( + shape_to_segment=test_shape_to_segment, + center_point=(500, 500), + number_of_segments=22, + ) + + assert test_shape.solid is not None + assert len(test_shape.solid.Solids()) == 22 + + def test_without_shape_to_segment(self): + """Checks a solid can be created if no shape is given + """ + test_shape = paramak.PoloidalSegments( + shape_to_segment=None, + center_point=(500, 500), + number_of_segments=22, + ) + + assert test_shape.solid is not None diff --git a/tests/test_parametric_components/test_PoloidallySegmentedBlanketFP.py b/tests/test_parametric_components/test_PoloidallySegmentedBlanketFP.py new file mode 100644 index 000000000..db18b4321 --- /dev/null +++ b/tests/test_parametric_components/test_PoloidallySegmentedBlanketFP.py @@ -0,0 +1,130 @@ + +import unittest + +import paramak + + +class TestBlanketFP(unittest.TestCase): + def test_creation(self): + blanket = paramak.BlanketFPPoloidalSegments( + thickness=20, start_angle=0, + stop_angle=180, rotation_angle=180) + assert blanket.solid is not None + + def test_creation_with_optimiser(self): + # Default + blanket = paramak.BlanketFPPoloidalSegments( + thickness=20, start_angle=0, + stop_angle=180, rotation_angle=180) + assert blanket.solid is not None + + # With limits + blanket.length_limits = (10, 300) + blanket.nb_segments_limits = (4, 8) + assert blanket.solid is not None + + # With None length_limits + blanket.length_limits = None + blanket.nb_segments_limits = (4, 8) + assert blanket.solid is not None + + # With None nb_segments_limits + blanket.start_angle = 80 + blanket.length_limits = (100, 300) + blanket.nb_segments_limits = None + assert blanket.solid is not None + + def test_optimiser(self): + blanket = paramak.BlanketFPPoloidalSegments( + thickness=20, start_angle=0, + stop_angle=180, rotation_angle=180) + + blanket.length_limits = (100, 300) + blanket.nb_segments_limits = (2, 8) + assert blanket.solid is not None + + def no_possible_config(): + blanket.length_limits = (10, 20) + blanket.nb_segments_limits = (2, 4) + blanket.solid + + self.assertRaises(ValueError, no_possible_config) + + def test_modifying_nb_segments_limits(self): + """creates a shape and checks that modifying the nb_segments_limits + also modifies segmets_angles accordingly + """ + blanket1 = paramak.BlanketFPPoloidalSegments( + thickness=20, start_angle=0, + stop_angle=180, rotation_angle=180) + + blanket2 = paramak.BlanketFPPoloidalSegments( + thickness=20, start_angle=0, + stop_angle=180, rotation_angle=180, + cut=blanket1) + + blanket1.nb_segments_limits = (4, 8) + blanket2.nb_segments_limits = (3, 8) + + assert blanket2.volume != 0 + + def test_segments_angles_is_modified_num_segments(self): + blanket1 = paramak.BlanketFPPoloidalSegments( + thickness=20, start_angle=0, + stop_angle=180, rotation_angle=180) + + blanket2 = paramak.BlanketFPPoloidalSegments( + thickness=20, start_angle=0, + stop_angle=180, rotation_angle=180, + cut=blanket1) + + blanket1.num_segments = 8 + blanket2.num_segments = 5 + assert blanket2.volume != 0 + + def test_num_point_is_affected(self): + blanket = paramak.BlanketFPPoloidalSegments( + thickness=20, start_angle=0, + stop_angle=180, rotation_angle=180) + assert blanket.num_points == blanket.num_segments + 1 + blanket.num_segments = 60 + assert blanket.num_points == blanket.num_segments + 1 + + def test_segment_angles_affects_solid(self): + blanket1 = paramak.BlanketFPPoloidalSegments( + thickness=20, start_angle=0, + stop_angle=180, rotation_angle=180) + blanket2 = paramak.BlanketFPPoloidalSegments( + thickness=20, start_angle=0, + stop_angle=180, rotation_angle=180, + cut=blanket1) + blanket2.segments_angles = [0, 25, 50, 90, 130, 150, 180] + assert blanket2.volume != 0 + + def test_warning_segment_angles(self): + blanket = paramak.BlanketFPPoloidalSegments( + thickness=20, start_angle=1, + stop_angle=50, rotation_angle=180) + + def warning1(): + blanket.start_angle = 1 + blanket.stop_angle = 50 + blanket.num_segments = None + blanket.segments_angles = [0, 25, 50, 90, 130, 150, 180] + + def warning2(): + blanket.start_angle = None + blanket.stop_angle = None + blanket.num_segments = 7 + blanket.segments_angles = [0, 25, 50, 90, 130, 150, 180] + + self.assertWarns(UserWarning, warning1) + self.assertWarns(UserWarning, warning2) + + def test_creation_with_gaps(self): + blanket = paramak.BlanketFPPoloidalSegments( + thickness=20, start_angle=0, + stop_angle=180, rotation_angle=180, + segments_gap=3 + ) + assert blanket.solid is not None diff --git a/tests/test_parametric_components/test_PortCutterCircular.py b/tests/test_parametric_components/test_PortCutterCircular.py new file mode 100644 index 000000000..a6d5e7197 --- /dev/null +++ b/tests/test_parametric_components/test_PortCutterCircular.py @@ -0,0 +1,19 @@ + +import unittest + +import paramak + + +# class test_component(unittest.TestCase): +# TODO: fix issue 548 +# def test_creation(self): +# """Checks a PortCutterCircular creation.""" + +# test_component = paramak.PortCutterCircular( +# distance=3, +# z_pos=0.25, +# radius=0.1, +# azimuth_placement_angle=[0, 45, 90, 180] +# ) + +# assert test_component.solid is not None diff --git a/tests/test_parametric_components/test_PortCutterRectangular.py b/tests/test_parametric_components/test_PortCutterRectangular.py new file mode 100644 index 000000000..0f927ccc3 --- /dev/null +++ b/tests/test_parametric_components/test_PortCutterRectangular.py @@ -0,0 +1,21 @@ + +import unittest + +import paramak + + +class TestPortCutterRectangular(unittest.TestCase): + + def test_creation(self): + """Checks a PortCutterRectangular creation.""" + + test_component = paramak.PortCutterRectangular( + distance=3, + z_pos=0, + height=0.2, + width=0.4, + fillet_radius=0.02, + azimuth_placement_angle=[0, 45, 90, 180] + ) + + assert test_component.solid is not None diff --git a/tests/test_parametric_components/test_PortCutterRotated.py b/tests/test_parametric_components/test_PortCutterRotated.py new file mode 100644 index 000000000..9c51912e2 --- /dev/null +++ b/tests/test_parametric_components/test_PortCutterRotated.py @@ -0,0 +1,160 @@ + +import unittest + +import numpy as np +import paramak + + +class TestPortCutterRotated(unittest.TestCase): + def test_shape_construction_and_volume(self): + """Cuts a vessel cylinder with several different size port cutters.""" + + small_ports = paramak.PortCutterRotated( + polar_coverage_angle=5, + center_point=(100, 0), + polar_placement_angle=10, + max_distance_from_center=1000, + azimuth_placement_angle=np.linspace(0, 360, 4), + rotation_angle=10 + ) + + large_ports = paramak.PortCutterRotated( + polar_coverage_angle=6, + center_point=(100, 0), + polar_placement_angle=10, + azimuth_placement_angle=np.linspace(0, 360, 4), + max_distance_from_center=1000, + rotation_angle=10 + ) + + vessel_with_out_ports = paramak.CenterColumnShieldCylinder( + height=500, + inner_radius=200, + outer_radius=300 + ) + + vessel_with_small_ports = paramak.CenterColumnShieldCylinder( + height=500, + inner_radius=200, + outer_radius=300, + cut=small_ports + ) + + vessel_with_large_ports = paramak.CenterColumnShieldCylinder( + height=500, + inner_radius=200, + outer_radius=300, + cut=large_ports + ) + + assert large_ports.volume > small_ports.volume + assert vessel_with_out_ports.volume > vessel_with_small_ports.volume + assert vessel_with_small_ports.volume > vessel_with_large_ports.volume + + def test_polar_coverage_angle_impacts_volume(self): + """Checks the volumes of two port cutters with different + polar_coverage_angle and checks angle impacts the volume.""" + + small_ports = paramak.PortCutterRotated( + polar_coverage_angle=5, + center_point=(100, 0), + polar_placement_angle=10, + max_distance_from_center=1000, + azimuth_placement_angle=np.linspace(0, 360, 4), + rotation_angle=10 + ) + + large_ports = paramak.PortCutterRotated( + polar_coverage_angle=6, + center_point=(100, 0), + polar_placement_angle=10, + max_distance_from_center=1000, + azimuth_placement_angle=np.linspace(0, 360, 4), + rotation_angle=10 + ) + + assert large_ports.volume > small_ports.volume + + def test_max_distance_from_center_impacts_volume(self): + """Checks the volumes of two port cutters with different + max_distance_from_center and checks distance impacts the volume.""" + + small_ports = paramak.PortCutterRotated( + polar_coverage_angle=5, + center_point=(100, 0), + polar_placement_angle=10, + max_distance_from_center=1000, + azimuth_placement_angle=np.linspace(0, 360, 4), + rotation_angle=10 + ) + + large_ports = paramak.PortCutterRotated( + polar_coverage_angle=5, + center_point=(100, 0), + polar_placement_angle=10, + max_distance_from_center=2000, + azimuth_placement_angle=np.linspace(0, 360, 4), + rotation_angle=10 + ) + + assert large_ports.volume > small_ports.volume + + def test_azimuth_placement_angle_impacts_volume(self): + """Checks the volumes of two port cutters with different + azimuth_placement_angle and checks distance impacts the volume.""" + + small_ports = paramak.PortCutterRotated( + polar_coverage_angle=5, + center_point=(100, 0), + polar_placement_angle=10, + max_distance_from_center=1000, + azimuth_placement_angle=np.linspace(0, 360, 4), + rotation_angle=10 + ) + + large_ports = paramak.PortCutterRotated( + polar_coverage_angle=5, + center_point=(100, 0), + polar_placement_angle=10, + max_distance_from_center=1000, + azimuth_placement_angle=np.linspace(0, 360, 5), + rotation_angle=10 + ) + + assert large_ports.volume > small_ports.volume + + def test_rotation_angle_impacts_volume(self): + """Checks the volumes of two port cutters with different + rotation_angle and checks distance impacts the volume.""" + + small_ports = paramak.PortCutterRotated( + polar_coverage_angle=5, + center_point=(100, 0), + polar_placement_angle=10, + max_distance_from_center=1000, + azimuth_placement_angle=np.linspace(0, 360, 4), + rotation_angle=10 + ) + + large_ports = paramak.PortCutterRotated( + polar_coverage_angle=5, + center_point=(100, 0), + polar_placement_angle=10, + max_distance_from_center=1000, + azimuth_placement_angle=np.linspace(0, 360, 4), + rotation_angle=20 + ) + + assert large_ports.volume > small_ports.volume + + def test_outerpoint_negative(self): + """Tests that when polar_coverage_angle is greater than 180 an error is + raised.""" + def error(): + shape = paramak.PortCutterRotated( + center_point=(1, 1), + polar_coverage_angle=181, + polar_placement_angle=0, + rotation_angle=10, + ) + self.assertRaises(ValueError, error) diff --git a/tests/test_parametric_components/test_ToroidalFieldCoilCoatHanger.py b/tests/test_parametric_components/test_ToroidalFieldCoilCoatHanger.py new file mode 100644 index 000000000..a4a042267 --- /dev/null +++ b/tests/test_parametric_components/test_ToroidalFieldCoilCoatHanger.py @@ -0,0 +1,159 @@ + +import unittest + +import paramak +import pytest + + +class TestToroidalFieldCoilCoatHanger(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.ToroidalFieldCoilCoatHanger( + horizontal_start_point=(200, 500), + horizontal_length=400, + vertical_mid_point=(700, 0), + vertical_length=500, + thickness=50, + distance=30, + number_of_coils=1, + with_inner_leg=True + ) + + def test_default_parameters(self): + """Checks that the default parameters of a ToroidalFieldCoilCoatHanger are correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.with_inner_leg + assert self.test_shape.stp_filename == "ToroidalFieldCoilCoatHanger.stp" + assert self.test_shape.stl_filename == "ToroidalFieldCoilCoatHanger.stl" + assert self.test_shape.material_tag == "outer_tf_coil_mat" + + def test_points_calculation(self): + """Checks that the points used to construct the ToroidalFieldCoilCoatHanger are + calculated correctly from the parameters given.""" + + assert self.test_shape.points == [ + (200, 500, 'straight'), (600, 500, 'straight'), (700, 250.0, 'straight'), + (700, -250.0, 'straight'), (600, -500, 'straight'), (200, -500, 'straight'), + (200, -550, 'straight'), (600, -550, 'straight'), + (646.423834544263, -518.5695338177052, 'straight'), + (746.423834544263, -268.5695338177052, 'straight'), (750, -250.0, 'straight'), + (750, 250.0, 'straight'), (746.423834544263, 268.5695338177052, 'straight'), + (646.423834544263, 518.5695338177052, 'straight'), (600, 550, 'straight'), + (200, 550, 'straight'), (200, 500, 'straight') + ] + + def test_creation_with_inner_leg(self): + """Creates a tf coil with inner leg using the ToroidalFieldCoilCoatHanger + parametric component and checks that a cadquery solid is created.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + assert self.test_shape.inner_leg_connection_points is not None + + test_inner_leg = paramak.ExtrudeStraightShape( + points=self.test_shape.inner_leg_connection_points, distance=30 + ) + assert test_inner_leg.solid is not None + + def test_creation_no_inner_leg(self): + """Creates a tf coil with no inner leg using the ToroidalFieldCoilRectangle + parametric component and checks that a cadquery solid is created.""" + + test_volume = self.test_shape.volume + + test_inner_leg = paramak.ExtrudeStraightShape( + points=self.test_shape.inner_leg_connection_points, distance=30 + ) + inner_leg_volume = test_inner_leg.volume + + self.test_shape.with_inner_leg = False + + assert self.test_shape.solid is not None + assert self.test_shape.volume == pytest.approx( + test_volume - inner_leg_volume) + + def test_absolute_volume(self): + """Creates a tf coil using the ToroidalFieldCoilCoatHanger parametric + component and checks that the volume is correc.""" + + assert self.test_shape.volume == pytest.approx((400 * 50 * 30 * 2) + + ((50 * 50 * 30 / 2) * 2) + (50 * 500 * 30) + + (((150 * 250 * 30) - (((100 * 250) / 2) * 30) - + (((100 * 250) / 2) * 30)) * 2) + (50 * 1000 * 30), rel=0.1) + + self.test_shape.with_inner_leg = False + assert self.test_shape.volume == pytest.approx((400 * 50 * 30 * 2) + + ((50 * 50 * 30 / 2) * 2) + + (50 * 500 * 30) + (((150 * 250 * 30) - + (((100 * 250) / 2) * 30) - + (((100 * 250) / 2) * 30)) * 2), rel=0.1) + + self.test_shape.with_inner_leg = True + self.test_shape.number_of_coils = 8 + assert self.test_shape.volume == pytest.approx(((400 * 50 * 30 * 2) + + ((50 * 50 * 30 / 2) * 2) + (50 * 500 * 30) + + (((150 * 250 * 30) - (((100 * 250) / 2) * 30) - + (((100 * 250) / 2) * 30)) * 2) + (50 * 1000 * 30)) * 8, rel=0.1) + + self.test_shape.with_inner_leg = False + assert self.test_shape.volume == pytest.approx(((400 * 50 * 30 * 2) + + ((50 * 50 * 30 / 2) * 2) + (50 * 500 * 30) + + (((150 * 250 * 30) - (((100 * 250) / 2) * 30) - + (((100 * 250) / 2) * 30)) * 2)) * 8, rel=0.1) + + def test_absolute_area(self): + """Creates a tf coil using the ToroidalFieldCoilCoatHanger parametric + component and checks that the areas of the faces are correct.""" + + assert self.test_shape.area == pytest.approx((((400 * 50 * 2) + + (50 * 50 * 0.5 * 2) + (((150 * 250) - (100 * 250 * 0.5) - + (100 * 250 * 0.5)) * 2) + (500 * 50)) * 2) + + ((50 * 30) * 4) + ((400 * 30) * 4) + ((500 * 30) * 2) + + ((((50**2 + 50**2)**0.5) * 30) * 2) + + ((((100**2 + 250**2)**0.5) * 30) * 4) + ((50 * 1000) * 2) + + ((1000 * 30) * 2), rel=0.1) + assert len(self.test_shape.areas) == 24 + + assert self.test_shape.areas.count(pytest.approx(50 * 30)) == 4 + assert self.test_shape.areas.count(pytest.approx(400 * 30)) == 4 + assert self.test_shape.areas.count(pytest.approx(500 * 30)) == 2 + assert self.test_shape.areas.count( + pytest.approx(((100**2 + 250**2)**0.5) * 30)) == 4 + assert self.test_shape.areas.count(pytest.approx(50 * 1000)) == 2 + assert self.test_shape.areas.count(pytest.approx(1000 * 30)) == 2 + + self.test_shape.with_inner_leg = False + assert self.test_shape.area == pytest.approx((((400 * 50 * 2) + + (50 * 50 * 0.5 * 2) + (((150 * 250) - (100 * 250 * 0.5) - + (100 * 250 * 0.5)) * 2) + (500 * 50)) * 2) + ((50 * 30) * 2) + + ((400 * 30) * 4) + ((500 * 30) * 2) + + ((((50**2 + 50**2)**0.5) * 30) * 2) + + ((((100**2 + 250**2)**0.5) * 30) * 4), rel=0.1) + + def test_rotation_angle(self): + """Creates a tf coil with a rotation_angle < 360 degrees and checks + that the correct cut is performed and the volume is correct.""" + + self.test_shape.number_of_coils = 8 + + self.test_shape.rotation_angle = 360 + self.test_shape.workplane = "XZ" + test_volume = self.test_shape.volume + self.test_shape.rotation_angle = 180 + assert self.test_shape.volume == pytest.approx( + test_volume * 0.5, rel=0.01) + + self.test_shape.rotation_angle = 360 + self.test_shape.workplane = "YZ" + test_volume = self.test_shape.volume + self.test_shape.rotation_angle = 180 + assert self.test_shape.volume == pytest.approx( + test_volume * 0.5, rel=0.01) + + self.test_shape.rotation_angle = 360 + self.test_shape.workplane = "XY" + self.test_shape.rotation_axis = "Y" + test_volume = self.test_shape.volume + self.test_shape.rotation_angle = 180 + assert self.test_shape.volume == pytest.approx(test_volume * 0.5) diff --git a/tests/test_parametric_components/test_ToroidalFieldCoilPrincetonD.py b/tests/test_parametric_components/test_ToroidalFieldCoilPrincetonD.py new file mode 100644 index 000000000..251f51505 --- /dev/null +++ b/tests/test_parametric_components/test_ToroidalFieldCoilPrincetonD.py @@ -0,0 +1,92 @@ + +import unittest + +import paramak +import pytest + + +class TestToroidalFieldCoilPrincetonD(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.ToroidalFieldCoilPrincetonD( + R1=100, R2=300, thickness=50, distance=30, number_of_coils=1, + with_inner_leg=True + ) + + def test_default_parameters(self): + """Checks that the default parameters of a ToroidalFieldCoilPrincetonD are correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.vertical_displacement == 0 + assert self.test_shape.with_inner_leg + assert self.test_shape.stp_filename == "ToroidalFieldCoilPrincetonD.stp" + assert self.test_shape.stl_filename == "ToroidalFieldCoilPrincetonD.stl" + assert self.test_shape.material_tag == "outer_tf_coil_mat" + + def test_creation_with_inner_leg(self): + """Creates a tf coil with inner leg using the ToroidalFieldCoilPrincetonD + parametric component and checks that a cadquery solid is created.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + assert self.test_shape.inner_leg_connection_points is not None + + test_inner_leg = paramak.ExtrudeStraightShape( + points=self.test_shape.inner_leg_connection_points, distance=30 + ) + assert test_inner_leg.solid is not None + + def test_creation_no_inner_leg(self): + """Creates a tf coil with no inner leg using the ToroidalFieldCoilPrincetonD + parametric component and checks that a cadquery solid is created.""" + + test_volume = self.test_shape.volume + + test_inner_leg = paramak.ExtrudeStraightShape( + points=self.test_shape.inner_leg_connection_points, distance=30 + ) + inner_leg_volume = test_inner_leg.volume + + self.test_shape.with_inner_leg = False + + assert self.test_shape.solid is not None + assert self.test_shape.volume < test_volume + + def test_relative_volume(self): + """Creates tf coil shapes with different numbers of tf coils and checks that + their relative volumes are correct.""" + + self.test_shape.number_of_coils = 4 + test_volume = self.test_shape.volume + + self.test_shape.number_of_coils = 8 + assert test_volume == pytest.approx( + self.test_shape.volume * 0.5, rel=0.01) + + def test_rotation_angle(self): + """Creates a tf coil with a rotation_angle < 360 degrees and checks + that the correct cut is performed and the volume is correct.""" + + self.test_shape.number_of_coils = 8 + + self.test_shape.rotation_angle = 360 + self.test_shape.workplane = "XZ" + test_volume = self.test_shape.volume + self.test_shape.rotation_angle = 180 + assert self.test_shape.volume == pytest.approx( + test_volume * 0.5, rel=0.01) + + self.test_shape.rotation_angle = 360 + self.test_shape.workplane = "YZ" + test_volume = self.test_shape.volume + self.test_shape.rotation_angle = 180 + assert self.test_shape.volume == pytest.approx( + test_volume * 0.5, rel=0.01) + + self.test_shape.rotation_angle = 360 + self.test_shape.workplane = "XY" + self.test_shape.rotation_axis = "Y" + test_volume = self.test_shape.volume + self.test_shape.rotation_angle = 180 + assert self.test_shape.volume == pytest.approx( + test_volume * 0.5, rel=0.01) diff --git a/tests/test_parametric_components/test_ToroidalFieldCoilRectangle.py b/tests/test_parametric_components/test_ToroidalFieldCoilRectangle.py new file mode 100644 index 000000000..14c3ff4e7 --- /dev/null +++ b/tests/test_parametric_components/test_ToroidalFieldCoilRectangle.py @@ -0,0 +1,158 @@ + +import unittest + +import paramak +import pytest + + +class TestToroidalFieldCoilRectangle(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.ToroidalFieldCoilRectangle( + horizontal_start_point=(100, 700), + vertical_mid_point=(800, 0), + thickness=50, + distance=30, + number_of_coils=1, + with_inner_leg=True + ) + + def test_default_parameters(self): + """Checks that the default parameters of a ToroidalFieldCoilRectangle are correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.with_inner_leg + assert self.test_shape.stp_filename == "ToroidalFieldCoilRectangle.stp" + assert self.test_shape.stl_filename == "ToroidalFieldCoilRectangle.stl" + assert self.test_shape.material_tag == "outer_tf_coil_mat" + + def test_points_calculation(self): + """Checks that the points used to construct the ToroidalFieldCoilRectangle are + calculated correctly from the parameters given.""" + + assert self.test_shape.points == [ + (100, 700, 'straight'), (150, 700, 'straight'), (800, 700, 'straight'), + (800, -700, 'straight'), (150, -700, 'straight'), (100, -700, 'straight'), + (100, -750, 'straight'), (850, -750, 'straight'), (850, 750, 'straight'), + (100, 750, 'straight'), (100, 700, 'straight') + ] + + def test_creation_with_inner_leg(self): + """Creates a tf coil with inner leg using the ToroidalFieldCoilRectangle + parametric component and checks that a cadquery solid is created.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + assert self.test_shape.inner_leg_connection_points is not None + + test_inner_leg = paramak.ExtrudeStraightShape( + points=self.test_shape.inner_leg_connection_points, distance=30 + ) + assert test_inner_leg.solid is not None + + def test_creation_no_inner_leg(self): + """Creates a tf coil with no inner leg using the ToroidalFieldCoilRectangle + parametric component and checks that a cadquery solid is created.""" + + test_volume = self.test_shape.volume + + test_inner_leg = paramak.ExtrudeStraightShape( + points=self.test_shape.inner_leg_connection_points, distance=30 + ) + inner_leg_volume = test_inner_leg.volume + + self.test_shape.with_inner_leg = False + assert self.test_shape.solid is not None + assert self.test_shape.volume == pytest.approx( + test_volume - inner_leg_volume) + + def test_absolute_volume(self): + """Creates a tf coil using the ToroidalFieldCoilRectangle parametric + component and checks that the volume is correct.""" + + self.test_shape.thickness = 150 + self.test_shape.distance = 50 + + assert self.test_shape.volume == pytest.approx( + (850 * 150 * 50 * 2) + (1400 * 150 * 50 * 2)) + + self.test_shape.with_inner_leg = False + assert self.test_shape.volume == pytest.approx( + (850 * 150 * 50 * 2) + (1400 * 150 * 50)) + + self.test_shape.with_inner_leg = True + self.test_shape.number_of_coils = 8 + assert self.test_shape.volume == pytest.approx( + ((850 * 150 * 50 * 2) + (1400 * 150 * 50 * 2)) * 8 + ) + + self.test_shape.with_inner_leg = False + assert self.test_shape.volume == pytest.approx( + ((850 * 150 * 50 * 2) + (1400 * 150 * 50)) * 8 + ) + + def test_absolute_areas(self): + """Creates tf coils using the ToroidalFieldCoilRectangle parametric + component and checks that the areas of the faces are correct.""" + + self.test_shape.thickness = 150 + self.test_shape.distance = 50 + + assert self.test_shape.area == pytest.approx((((850 * 150 * 2) + (1400 * 150)) * 2) + ( + 1400 * 150 * 2) + (850 * 50 * 2) + (1700 * 50) + (1400 * 50 * 3) + (700 * 50 * 2) + (150 * 50 * 4)) + assert len(self.test_shape.areas) == 16 + assert self.test_shape.areas.count(pytest.approx( + (850 * 150 * 2) + (1400 * 150))) == 2 + assert self.test_shape.areas.count(pytest.approx((1400 * 150))) == 2 + assert self.test_shape.areas.count(pytest.approx(850 * 50)) == 2 + assert self.test_shape.areas.count(pytest.approx(1700 * 50)) == 1 + assert self.test_shape.areas.count(pytest.approx(1400 * 50)) == 3 + assert self.test_shape.areas.count(pytest.approx(700 * 50)) == 2 + assert self.test_shape.areas.count(pytest.approx(150 * 50)) == 4 + + self.test_shape.with_inner_leg = False + assert self.test_shape.area == pytest.approx((((850 * 150 * 2) + (1400 * 150)) * 2) + ( + 850 * 50 * 2) + (1700 * 50) + (1400 * 50) + (700 * 50 * 2) + (150 * 50 * 2)) + + def test_rotation_angle(self): + """Creates tf coils with rotation_angles < 360 degrees in different + workplanes and checks that the correct cuts are performed and their + volumes are correct.""" + + self.test_shape.number_of_coils = 8 + + self.test_shape.rotation_angle = 360 + self.test_shape.workplane = "XZ" + test_volume = self.test_shape.volume + self.test_shape.rotation_angle = 180 + assert self.test_shape.volume == pytest.approx(test_volume * 0.5) + + self.test_shape.rotation_angle = 360 + self.test_shape.workplane = "YZ" + test_volume = self.test_shape.volume + self.test_shape.rotation_angle = 180 + assert self.test_shape.volume == pytest.approx(test_volume * 0.5) + + self.test_shape.rotation_angle = 360 + self.test_shape.workplane = "XY" + self.test_shape.rotation_axis = "Y" + test_volume = self.test_shape.volume + self.test_shape.rotation_angle = 180 + assert self.test_shape.volume == pytest.approx(test_volume * 0.5) + + def test_error(self): + """Checks errors are raised with invalid arguments.""" + + def incorrect_horizontal_start_point(): + self.test_shape.vertical_mid_point = (800, 0) + self.test_shape.horizontal_start_point = (801, 700) + self.test_shape.solid + + self.assertRaises(ValueError, incorrect_horizontal_start_point) + + def incorrect_vertical_mid_point(): + self.test_shape.horizontal_start_point = (100, 700) + self.test_shape.vertical_mid_point = (800, 701) + self.test_shape.solid + + self.assertRaises(ValueError, incorrect_vertical_mid_point) diff --git a/tests/test_parametric_components/test_ToroidalFieldCoilTripleArc.py b/tests/test_parametric_components/test_ToroidalFieldCoilTripleArc.py new file mode 100644 index 000000000..bbc14ad20 --- /dev/null +++ b/tests/test_parametric_components/test_ToroidalFieldCoilTripleArc.py @@ -0,0 +1,94 @@ + +import unittest + +import paramak +import pytest + + +class TestToroidalFieldCoilTripleArc(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.ToroidalFieldCoilTripleArc( + R1=100, h=100, radii=(100, 200), coverages=(10, 60), thickness=10, + distance=50, number_of_coils=1, + ) + + def test_default_parameters(self): + """Checks that the default parameters of a ToroidalFieldCoilTripleArc are correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.with_inner_leg + assert self.test_shape.vertical_displacement == 0 + assert self.test_shape.stp_filename == "ToroidalFieldCoilTripleArc.stp" + assert self.test_shape.stl_filename == "ToroidalFieldCoilTripleArc.stl" + assert self.test_shape.material_tag == "outer_tf_coil_mat" + + def test_creation_with_inner_leg(self): + """Creates a tf coil with inner leg using the ToroidalFieldCoilTripleArc + parametric component and checks that a cadquery solid is created.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + assert self.test_shape.inner_leg_connection_points is not None + + test_inner_leg = paramak.ExtrudeStraightShape( + points=self.test_shape.inner_leg_connection_points, distance=50 + ) + assert test_inner_leg.solid is not None + + def test_creation_no_inner_leg(self): + """Creates a tf coil with no inner leg using the ToroidalFieldCoilRectangle + parametric component and checks that a cadquery solid is created.""" + + test_volume = self.test_shape.volume + + test_inner_leg = paramak.ExtrudeStraightShape( + points=self.test_shape.inner_leg_connection_points, distance=50 + ) + inner_leg_volume = test_inner_leg.volume + + self.test_shape.with_inner_leg = False + + assert self.test_shape.solid is not None + assert self.test_shape.volume == pytest.approx( + test_volume - inner_leg_volume) + + def test_relative_volume(self): + """Creates tf coil shapes with different numbers of tf coils and checks that + their relative volumes are correct.""" + + test_volume = self.test_shape.volume + + self.test_shape.number_of_coils = 8 + + assert self.test_shape.volume == pytest.approx( + test_volume * 8, rel=0.01) + + def test_rotation_angle(self): + """Creates tf coils with rotation_angles < 360 in different workplanes + and checks that the correct cuts are performed and their volumes are + correct.""" + + self.test_shape.number_of_coils = 8 + + self.test_shape.rotation_angle = 360 + self.test_shape.workplane = "XZ" + test_volume = self.test_shape.volume + self.test_shape.rotation_angle = 180 + assert self.test_shape.volume == pytest.approx( + test_volume * 0.5, rel=0.01) + + self.test_shape.rotation_angle = 360 + self.test_shape.workplane = "YZ" + test_volume = self.test_shape.volume + self.test_shape.rotation_angle = 180 + assert self.test_shape.volume == pytest.approx( + test_volume * 0.5, rel=0.01) + + self.test_shape.rotation_angle = 360 + self.test_shape.workplane = "XY" + self.test_shape.rotation_axis = "Y" + test_volume = self.test_shape.volume + self.test_shape.rotation_angle = 180 + assert self.test_shape.volume == pytest.approx( + test_volume * 0.5, rel=0.01) diff --git a/tests/test_parametric_components/test_VacuumVessel.py b/tests/test_parametric_components/test_VacuumVessel.py new file mode 100644 index 000000000..7026be32d --- /dev/null +++ b/tests/test_parametric_components/test_VacuumVessel.py @@ -0,0 +1,42 @@ + +import unittest + +import paramak + + +class TestVacuumVessel(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.VacuumVessel( + height=2, inner_radius=1, thickness=0.2 + ) + + def test_creation(self): + """Creates a shape using the VacuumVessel parametric component and + checks that a cadquery solid is created.""" + + assert self.test_shape.solid is not None + + def test_ports(self): + """Creates a vacuum vessel with ports holes in it and checks that a + caquery solid is created.""" + + cutter1 = paramak.PortCutterRectangular( + distance=3, z_pos=0, height=0.2, width=0.4, fillet_radius=0.01) + cutter2 = paramak.PortCutterRectangular( + distance=3, z_pos=0.5, height=0.2, width=0.4, fillet_radius=0.00) + cutter3 = paramak.PortCutterRectangular( + distance=3, z_pos=-0.5, height=0.2, width=0.4, + physical_groups=None) + cutter4 = paramak.PortCutterCircular( + distance=3, z_pos=0.25, radius=0.1, azimuth_placement_angle=45, + physical_groups=None) + cutter5 = paramak.PortCutterRotated( + (0, 0), azimuth_placement_angle=-90, rotation_angle=10, + fillet_radius=0.01, physical_groups=None) + + pre_cut_volume = self.test_shape.volume + + self.test_shape.cut = [cutter1, cutter2, cutter3, cutter4, cutter5] + assert self.test_shape.solid is not None + assert self.test_shape.volume < pre_cut_volume diff --git a/tests/test_parametric_components/test_casing.py b/tests/test_parametric_components/test_casing.py new file mode 100644 index 000000000..961014f23 --- /dev/null +++ b/tests/test_parametric_components/test_casing.py @@ -0,0 +1,26 @@ + +import unittest + +import paramak + + +class TestTFCoilCasing(unittest.TestCase): + + def test_creation(self): + inner_offset = 10 + outer_offset = 10 + magnet_thickness = 5 + magnet_extrude_distance = 10 + vertical_section_offset = 20 + casing_extrude_distance = magnet_extrude_distance * 2 + + # create a princeton D magnet + magnet = paramak.ToroidalFieldCoilPrincetonD( + R1=100, R2=200, thickness=magnet_thickness, + distance=magnet_extrude_distance, number_of_coils=1) + + casing = paramak.TFCoilCasing( + magnet=magnet, distance=casing_extrude_distance, + inner_offset=inner_offset, outer_offset=outer_offset, + vertical_section_offset=vertical_section_offset) + assert casing.solid is not None diff --git a/tests/test_parametric_neutronics/__init__.py b/tests/test_parametric_neutronics/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_parametric_neutronics/test_NeutronicModelFromReactor.py b/tests/test_parametric_neutronics/test_NeutronicModelFromReactor.py new file mode 100644 index 000000000..e3dcd32c3 --- /dev/null +++ b/tests/test_parametric_neutronics/test_NeutronicModelFromReactor.py @@ -0,0 +1,218 @@ + +import os +import unittest +from pathlib import Path + +import neutronics_material_maker as nmm +import openmc +import paramak +import pytest + + +class TestNeutronicsBallReactor(unittest.TestCase): + """Tests the neutronicsModelFromReactor including neutronics simulations""" + + # makes the 3d geometry + my_reactor = paramak.BallReactor( + inner_bore_radial_thickness=1, + inboard_tf_leg_radial_thickness=30, + center_column_shield_radial_thickness=60, + divertor_radial_thickness=50, + inner_plasma_gap_radial_thickness=30, + plasma_radial_thickness=300, + outer_plasma_gap_radial_thickness=30, + firstwall_radial_thickness=3, + blanket_radial_thickness=100, + blanket_rear_wall_radial_thickness=3, + elongation=2.75, + triangularity=0.5, + rotation_angle=360, + ) + + # makes a homogenised material for the blanket from lithium lead and + # eurofer + blanket_material = nmm.MultiMaterial( + fracs=[0.8, 0.2], + materials=[ + nmm.Material('SiC'), + nmm.Material('eurofer') + ]) + + source = openmc.Source() + # sets the location of the source to x=0 y=0 z=0 + source.space = openmc.stats.Point((0, 0, 0)) + # sets the direction to isotropic + source.angle = openmc.stats.Isotropic() + # sets the energy distribution to 100% 14MeV neutrons + source.energy = openmc.stats.Discrete([14e6], [1]) + + def test_neutronics_model_attributes(self): + """Makes a BallReactor neutronics model and simulates the TBR""" + + # makes the neutronics material + neutronics_model = paramak.NeutronicsModelFromReactor( + reactor=self.my_reactor, + source=openmc.Source(), + materials={ + 'inboard_tf_coils_mat': 'copper', + 'center_column_shield_mat': 'WC', + 'divertor_mat': 'eurofer', + 'firstwall_mat': 'eurofer', + 'blanket_mat': self.blanket_material, # use of homogenised material + 'blanket_rear_wall_mat': 'eurofer'}, + cell_tallies=['TBR', 'flux', 'heating'], + simulation_batches=42, + simulation_particles_per_batch=84, + ) + + assert neutronics_model.reactor == self.my_reactor + + assert neutronics_model.materials == { + 'inboard_tf_coils_mat': 'copper', + 'center_column_shield_mat': 'WC', + 'divertor_mat': 'eurofer', + 'firstwall_mat': 'eurofer', + 'blanket_mat': self.blanket_material, + 'blanket_rear_wall_mat': 'eurofer'} + + assert neutronics_model.cell_tallies == ['TBR', 'flux', 'heating'] + + assert neutronics_model.simulation_batches == 42 + assert isinstance(neutronics_model.simulation_batches, int) + + assert neutronics_model.simulation_particles_per_batch == 84 + assert isinstance(neutronics_model.simulation_particles_per_batch, int) + + def test_reactor_from_shapes_cell_tallies(self): + """Makes a reactor from two shapes, then mades a neutronics model + and tests the TBR simulation value""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], + material_tag='mat1', + ) + test_shape2 = paramak.RotateSplineShape( + points=[(100, 100), (100, -100), (200, -100), (200, 100)], + material_tag='blanket_mat', + rotation_angle=180 + ) + + test_reactor = paramak.Reactor([test_shape, test_shape2]) + test_reactor.rotation_angle = 360 + + neutronics_model = paramak.NeutronicsModelFromReactor( + reactor=test_reactor, + source=self.source, + materials={ + 'mat1': 'copper', + 'blanket_mat': 'FLiNaK', # used as O18 is not in nndc nuc data + }, + cell_tallies=['TBR', 'heating', 'flux'], + simulation_batches=5, + simulation_particles_per_batch=1e3, + ) + + # starts the neutronics simulation using trelis + neutronics_model.simulate(verbose=False, method='pymoab') + + def test_reactor_from_shapes_2d_mesh_tallies(self): + """Makes a reactor from two shapes, then mades a neutronics model + and tests the TBR simulation value""" + + os.system('rm *.png') + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], + material_tag='mat1', + ) + test_shape2 = paramak.RotateSplineShape( + points=[(100, 100), (100, -100), (200, -100), (200, 100)], + material_tag='blanket_mat', + rotation_angle=180 + ) + + test_reactor = paramak.Reactor([test_shape, test_shape2]) + test_reactor.rotation_angle = 360 + + neutronics_model = paramak.NeutronicsModelFromReactor( + reactor=test_reactor, + source=self.source, + materials={ + 'mat1': 'copper', + 'blanket_mat': 'FLiNaK', # used as O18 is not in nndc nuc data + }, + mesh_tally_2D=['tritium_production', 'heating', 'flux'], + simulation_batches=2, + simulation_particles_per_batch=10, + ) + + # starts the neutronics simulation using trelis + neutronics_model.simulate(verbose=False, method='pymoab') + neutronics_model.get_results() + + assert Path("tritium_production_on_2D_mesh_xz.png").exists() is True + assert Path("tritium_production_on_2D_mesh_xy.png").exists() is True + assert Path("tritium_production_on_2D_mesh_yz.png").exists() is True + assert Path("heating_on_2D_mesh_xz.png").exists() is True + assert Path("heating_on_2D_mesh_xy.png").exists() is True + assert Path("heating_on_2D_mesh_yz.png").exists() is True + assert Path("flux_on_2D_mesh_xz.png").exists() is True + assert Path("flux_on_2D_mesh_xy.png").exists() is True + assert Path("flux_on_2D_mesh_yz.png").exists() is True + + def test_incorrect_settings(self): + """Creates NeutronicsModelFromReactor objects and checks errors are + raised correctly when arguments are incorrect.""" + + def test_incorrect_method(): + """Makes a BallReactor neutronics model and simulates the TBR""" + + # makes the neutronics material + neutronics_model = paramak.NeutronicsModelFromReactor( + reactor=self.my_reactor, + source=self.source, + materials={ + 'inboard_tf_coils_mat': 'copper', + 'center_column_shield_mat': 'WC', + 'divertor_mat': 'eurofer', + 'firstwall_mat': 'eurofer', + 'blanket_mat': 'FLiNaK', # used as O18 is not in nndc nuc data + 'blanket_rear_wall_mat': 'eurofer'}, + cell_tallies=['TBR', 'flux', 'heating'], + simulation_batches=42, + simulation_particles_per_batch=84, + ) + + neutronics_model.create_neutronics_geometry(method='incorrect') + + self.assertRaises(ValueError, test_incorrect_method) + + # def test_tbr_simulation(self): + + # def test_tbr_simulation(self): + # """Makes a BallReactor neutronics model and simulates the TBR""" + + # makes the neutronics material + # neutronics_model = paramak.NeutronicsModelFromReactor( + # reactor=my_reactor, + # materials={ + # 'inboard_tf_coils_mat': 'copper', + # 'center_column_shield_mat': 'WC', + # 'divertor_mat': 'eurofer', + # 'firstwall_mat': 'eurofer', + # 'blanket_mat': blanket_material, # use of homogenised material + # 'blanket_rear_wall_mat': 'eurofer'}, + # cell_tallies=['TBR'], + # simulation_batches=5, + # simulation_particles_per_batch=1e3, + # ) + + # starts the neutronics simulation using trelis + # neutronics_model.simulate(method='trelis') + + # assert neutronics_model.results['TBR']['result'] == pytest.approx( + # 1.168, rel=0.2) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_parametric_neutronics/test_Reactor_neutronics.py b/tests/test_parametric_neutronics/test_Reactor_neutronics.py new file mode 100644 index 000000000..0ce80d466 --- /dev/null +++ b/tests/test_parametric_neutronics/test_Reactor_neutronics.py @@ -0,0 +1,79 @@ + +import os +import unittest +from pathlib import Path + +import paramak + + +class TestReactorNeutronics(unittest.TestCase): + + def test_export_h5m(self): + """Creates a Reactor object consisting of two shapes and checks a h5m + file of the reactor can be exported using the export_h5m method.""" + + os.system('rm small_dagmc.h5m') + os.system('rm small_dagmc_without_graveyard.h5m') + os.system('rm small_dagmc_with_graveyard.h5m') + os.system('rm large_dagmc.h5m') + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], + material_tag='mat1') + test_shape2 = paramak.RotateSplineShape( + points=[(0, 0), (0, 20), (20, 20)], + material_tag='mat2') + test_shape.rotation_angle = 360 + test_reactor = paramak.Reactor([test_shape, test_shape2]) + test_reactor.export_h5m(filename='small_dagmc.h5m', tolerance=0.01) + test_reactor.export_h5m( + filename='small_dagmc_without_graveyard.h5m', + tolerance=0.01, + skip_graveyard=True) + test_reactor.export_h5m( + filename='small_dagmc_with_graveyard.h5m', + tolerance=0.01, + skip_graveyard=False) + test_reactor.export_h5m(filename='large_dagmc.h5m', tolerance=0.001) + + assert Path("small_dagmc.h5m").exists() is True + assert Path("small_dagmc_with_graveyard.h5m").exists() is True + assert Path("large_dagmc.h5m").exists() is True + assert Path("large_dagmc.h5m").stat().st_size > Path( + "small_dagmc.h5m").stat().st_size + assert Path("small_dagmc_without_graveyard.h5m").stat( + ).st_size < Path("small_dagmc.h5m").stat().st_size + + def test_export_h5m_without_extension(self): + """Tests that the code appends .h5m to the end of the filename""" + + os.system('rm out.h5m') + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], + material_tag='mat1') + test_shape2 = paramak.RotateSplineShape( + points=[(0, 0), (0, 20), (20, 20)], + material_tag='mat2') + test_shape.rotation_angle = 360 + test_reactor = paramak.Reactor([test_shape, test_shape2]) + test_reactor.export_h5m(filename='out', tolerance=0.01) + assert Path("out.h5m").exists() is True + os.system('rm out.h5m') + + def test_offset_from_graveyard_sets_attribute(self): + """Creates a graveyard for a reactor and sets the graveyard_offset. + Checks that the Reactor.graveyard_offset property is set""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)], + material_tag='mat1') + test_shape2 = paramak.RotateSplineShape( + points=[(0, 0), (0, 20), (20, 20)], + material_tag='mat2') + test_shape.rotation_angle = 360 + test_reactor = paramak.Reactor([test_shape, test_shape2]) + test_reactor.make_graveyard(graveyard_offset=101) + assert test_reactor.graveyard_offset == 101 + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_parametric_neutronics/test_Shape_neutronics.py b/tests/test_parametric_neutronics/test_Shape_neutronics.py new file mode 100644 index 000000000..6d28c736c --- /dev/null +++ b/tests/test_parametric_neutronics/test_Shape_neutronics.py @@ -0,0 +1,73 @@ + +import os +import unittest +from pathlib import Path + +import paramak + + +class test_object_properties(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.ExtrudeMixedShape( + points=[ + (50, 0, "straight"), + (50, 50, "spline"), + (60, 70, "spline"), + (70, 50, "circle"), + (60, 25, "circle"), + (70, 0, "straight")], + distance=50 + ) + + def test_export_h5m_creates_file(self): + """Tests the Shape.export_h5m method results in an outputfile.""" + os.system('rm test_shape.h5m') + self.test_shape.export_h5m(filename='test_shape.h5m') + assert Path("test_shape.h5m").exists() is True + + def test_export_h5m_creates_file_even_without_extention(self): + """Tests the Shape.export_h5m method results in an outputfile even + when the filename does not include the .h5m""" + os.system('rm test_shape.h5m') + self.test_shape.export_h5m(filename='test_shape') + assert Path("test_shape.h5m").exists() is True + + def test_offset_from_graveyard_sets_attribute(self): + os.system('rm test_shape.h5m') + self.test_shape.export_h5m( + filename='test_shape.h5m', + graveyard_offset=101) + assert self.test_shape.graveyard_offset == 101 + + def test_tolerance_increases_filesize(self): + os.system('rm test_shape.h5m') + self.test_shape.export_h5m( + filename='test_shape_0001.h5m', + tolerance=0.001) + self.test_shape.export_h5m( + filename='test_shape_001.h5m', + tolerance=0.01) + assert Path('test_shape_0001.h5m').stat().st_size > Path( + 'test_shape_001.h5m').stat().st_size + + def test_skipping_graveyard_decreases_filesize(self): + os.system('rm test_shape.h5m') + self.test_shape.export_h5m(filename='skiped.h5m', skip_graveyard=True) + self.test_shape.export_h5m( + filename='not_skipped.h5m', + skip_graveyard=False) + assert Path('not_skipped.h5m').stat().st_size > Path( + 'skiped.h5m').stat().st_size + + def test_graveyard_offset_increases_voulme(self): + os.system('rm test_shape.h5m') + self.test_shape.make_graveyard(graveyard_offset=100) + small_offset = self.test_shape.graveyard.volume + self.test_shape.make_graveyard(graveyard_offset=1000) + large_offset = self.test_shape.graveyard.volume + assert small_offset < large_offset + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_parametric_reactors/__init__.py b/tests/test_parametric_reactors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_parametric_reactors/test_BallReactor.py b/tests/test_parametric_reactors/test_BallReactor.py new file mode 100644 index 000000000..e447c48aa --- /dev/null +++ b/tests/test_parametric_reactors/test_BallReactor.py @@ -0,0 +1,226 @@ + +import os +import time +import unittest +import warnings +from pathlib import Path + +import paramak + + +class TestBallReactor(unittest.TestCase): + + def setUp(self): + self.test_reactor = paramak.BallReactor( + inner_bore_radial_thickness=50, + inboard_tf_leg_radial_thickness=200, + center_column_shield_radial_thickness=50, + divertor_radial_thickness=100, + inner_plasma_gap_radial_thickness=150, + plasma_radial_thickness=100, + outer_plasma_gap_radial_thickness=50, + firstwall_radial_thickness=50, + blanket_radial_thickness=100, + blanket_rear_wall_radial_thickness=10, + elongation=2, + triangularity=0.55, + number_of_tf_coils=16, + rotation_angle=180, + ) + + def test_creation_with_narrow_divertor(self): + """Creates a BallReactor with a narrow divertor and checks that the correct + number of components are created.""" + + self.test_reactor.divertor_radial_thickness = 50 + + assert self.test_reactor.solid is not None + assert len(self.test_reactor.shapes_and_components) == 7 + + def test_creation_with_narrow_divertor(self): + """Creates a BallReactor with a wide divertor and checks that the correct + number of components are created.""" + + self.test_reactor.divertor_radial_thickness = 172.5 + + assert self.test_reactor.solid is not None + assert len(self.test_reactor.shapes_and_components) == 7 + + def test_svg_creation(self): + """Creates a BallReactor and checks that an svg image of the reactor can be + exported using the export_svg method.""" + + os.system("rm test_ballreactor_image.svg") + self.test_reactor.export_svg("filename.svg") + assert Path("filename.svg").exists() is True + os.system("rm filename.svg") + + def test_with_pf_coils(self): + """Checks that a BallReactor with optional pf coils can be created and that + the correct number of components are created.""" + + self.test_reactor.pf_coil_radial_thicknesses = [50, 50, 50, 50] + self.test_reactor.pf_coil_vertical_thicknesses = [50, 50, 50, 50] + self.test_reactor.pf_coil_to_rear_blanket_radial_gap = 50 + self.test_reactor.pf_coil_case_thickness = 10 + + assert self.test_reactor.solid is not None + assert len(self.test_reactor.shapes_and_components) == 9 + + def test_pf_coil_thicknesses_error(self): + """Checks that an error is raised when invalid pf_coil_radial_thicknesses and + pf_coil_vertical_thicknesses are specified.""" + + def invalid_pf_coil_radial_thicknesses(): + self.test_reactor.pf_coil_radial_thicknesses = 2 + self.assertRaises(ValueError, invalid_pf_coil_radial_thicknesses) + + def invalid_pf_coil_vertical_thicknesses(): + self.test_reactor.pf_coil_vertical_thicknesses = 2 + self.assertRaises(ValueError, invalid_pf_coil_vertical_thicknesses) + + def test_with_pf_and_tf_coils(self): + """Checks that a BallReactor with optional pf and tf coils can be created and + that the correct number of components are created.""" + + self.test_reactor.pf_coil_radial_thicknesses = [50, 50, 50, 50] + self.test_reactor.pf_coil_vertical_thicknesses = [50, 50, 50, 50] + self.test_reactor.pf_coil_to_rear_blanket_radial_gap = 50 + self.test_reactor.pf_coil_to_tf_coil_radial_gap = 50 + self.test_reactor.pf_coil_case_thickness = 10 + self.test_reactor.outboard_tf_coil_radial_thickness = 50 + self.test_reactor.outboard_tf_coil_poloidal_thickness = 50 + + assert self.test_reactor.solid is not None + assert len(self.test_reactor.shapes_and_components) == 10 + + def test_with_pf_and_tf_coils_export_physical_groups(self): + """Creates a BallReactor and checks that the export_physical_groups method + works correctly.""" + + self.test_reactor.export_physical_groups() + + # insert assertion + + def test_rotation_angle_warning(self): + """Creates a BallReactor with rotation_angle = 360 and checks that the correct + warning message is printed.""" + + def warning_trigger(): + self.test_reactor.rotation_angle = 360 + self.test_reactor._rotation_angle_check() + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + warning_trigger() + assert len(w) == 1 + assert issubclass(w[-1].category, UserWarning) + assert "360 degree rotation may result in a Standard_ConstructionError or AttributeError" in str( + w[-1].message) + + def test_ball_reactor_hash_value(self): + """Creates a ball reactor and checks that all shapes in the reactor are created + when .shapes_and_components is first called. Checks that when .shapes_and_components + is called again with no changes to the reactor, the shapes in the reactor are not + reconstructed and the previously constructed shapes are returned. Checks that when + .shapes_and_components is called again with changes to the reactor, the shapes + in the reactor are reconstructed and these new shapes are returned. Checks that + the reactor_hash_value is only updated when the reactor is reconstructed.""" + + self.test_reactor.pf_coil_radial_thicknesses = [50, 50, 50, 50] + self.test_reactor.pf_coil_vertical_thicknesses = [50, 50, 50, 50] + self.test_reactor.pf_coil_to_rear_blanket_radial_gap = 50 + self.test_reactor.pf_coil_to_tf_coil_radial_gap = 50 + self.test_reactor.pf_coil_case_thickness = 10 + self.test_reactor.outboard_tf_coil_radial_thickness = 100 + self.test_reactor.outboard_tf_coil_poloidal_thickness = 50 + + assert self.test_reactor.reactor_hash_value is None + for key in [ + "_plasma", + "_inboard_tf_coils", + "_center_column_shield", + "_divertor", + "_firstwall", + "_blanket", + "_blanket_rear_wall", + "_pf_coil", + "_pf_coils_casing", + "_tf_coil" + ]: + assert key not in self.test_reactor.__dict__.keys() + assert self.test_reactor.shapes_and_components is not None + + for key in [ + "_plasma", + "_inboard_tf_coils", + "_center_column_shield", + "_divertor", + "_firstwall", + "_blanket", + "_blanket_rear_wall", + "_pf_coil", + "_pf_coils_casing", + "_tf_coil" + ]: + assert key in self.test_reactor.__dict__.keys() + assert len(self.test_reactor.shapes_and_components) == 10 + assert self.test_reactor.reactor_hash_value is not None + initial_hash_value = self.test_reactor.reactor_hash_value + self.test_reactor.rotation_angle = 270 + assert self.test_reactor.reactor_hash_value == initial_hash_value + assert self.test_reactor.shapes_and_components is not None + assert self.test_reactor.reactor_hash_value != initial_hash_value + + def test_hash_value_time_saving(self): + """Checks that use of conditional reactor reconstruction via the hash value + gives the expected time saving.""" + + self.test_reactor.pf_coil_radial_thicknesses = [50, 50, 50, 50] + self.test_reactor.pf_coil_vertical_thicknesses = [50, 50, 50, 50] + self.test_reactor.pf_coil_to_rear_blanket_radial_gap = 50 + self.test_reactor.pf_coil_to_tf_coil_radial_gap = 50 + self.test_reactor.pf_coil_case_thickness = 10 + self.test_reactor.outboard_tf_coil_radial_thickness = 100 + self.test_reactor.outboard_tf_coil_poloidal_thickness = 50 + + start_time = time.time() + self.test_reactor.shapes_and_components + stop_time = time.time() + initial_construction_time = stop_time - start_time + + start_time = time.time() + self.test_reactor.shapes_and_components + stop_time = time.time() + reconstruction_time = stop_time - start_time + + assert reconstruction_time < initial_construction_time + # assert reconstruction_time < initial_construction_time * 0.01 + + def test_divertor_position_error(self): + """checks an invalid divertor position raises the correct + ValueError.""" + + def invalid_position(): + self.test_reactor.divertor_position = "coucou" + + self.assertRaises(ValueError, invalid_position) + + def test_divertor_upper_lower(self): + """Checks that BallReactors with coils with lower and upper divertors + can be created.""" + self.test_reactor.pf_coil_radial_thicknesses = [50, 50, 50, 50] + self.test_reactor.pf_coil_vertical_thicknesses = [50, 50, 50, 50] + self.test_reactor.pf_coil_to_rear_blanket_radial_gap = 50 + self.test_reactor.pf_coil_to_tf_coil_radial_gap = 50 + self.test_reactor.pf_coil_case_thickness = 10 + self.test_reactor.outboard_tf_coil_radial_thickness = 50 + self.test_reactor.outboard_tf_coil_poloidal_thickness = 50 + + self.test_reactor.divertor_position = "lower" + assert self.test_reactor.solid is not None + assert len(self.test_reactor.shapes_and_components) == 10 + + self.test_reactor.divertor_position = "upper" + assert self.test_reactor.solid is not None + assert len(self.test_reactor.shapes_and_components) == 10 diff --git a/tests/test_parametric_reactors/test_CenterColumnStudyReactor.py b/tests/test_parametric_reactors/test_CenterColumnStudyReactor.py new file mode 100644 index 000000000..d0bbb65f3 --- /dev/null +++ b/tests/test_parametric_reactors/test_CenterColumnStudyReactor.py @@ -0,0 +1,84 @@ + +import os +import unittest +import warnings +from pathlib import Path + +import paramak +import pytest + + +class TestCenterColumnStudyReactor(unittest.TestCase): + + def setUp(self): + self.test_reactor = paramak.CenterColumnStudyReactor( + inner_bore_radial_thickness=20, + inboard_tf_leg_radial_thickness=50, + center_column_shield_radial_thickness_mid=50, + center_column_shield_radial_thickness_upper=100, + inboard_firstwall_radial_thickness=20, + divertor_radial_thickness=100, + inner_plasma_gap_radial_thickness=80, + plasma_radial_thickness=200, + outer_plasma_gap_radial_thickness=90, + elongation=2.3, + triangularity=0.45, + plasma_gap_vertical_thickness=40, + center_column_arc_vertical_thickness=520, + rotation_angle=359 + ) + + def test_creation(self): + """Creates a ball reactor using the CenterColumnStudyReactor parametric_reactor and checks + the correct number of components are created.""" + + assert len(self.test_reactor.shapes_and_components) == 6 + + def test_creation_with_narrow_divertor(self): + """Creates a ball reactor with a narrow divertor using the CenterColumnStudyReactor + parametric reactor and checks that the correct number of components are created.""" + + self.test_reactor.divertor_radial_thickness = 10 + assert len(self.test_reactor.shapes_and_components) == 6 + + def test_svg_creation(self): + """Creates a ball reactor using the CenterColumnStudyReactor parametric_reactor and checks + an svg image of the reactor can be exported.""" + + os.system("rm test_image.svg") + self.test_reactor.export_svg("test_image.svg") + assert Path("test_image.svg").exists() is True + os.system("rm test_image.svg") + + def test_rotation_angle_impacts_volume(self): + """Creates a CenterColumnStudyReactor reactor with a rotation angle of + 90 and another reactor with a rotation angle of 180. Then checks the + volumes of all the components is double in the 180 reactor.""" + + self.test_reactor.rotation_angle = 90 + r90_comp_vols = [ + comp.volume for comp in self.test_reactor.shapes_and_components] + self.test_reactor.rotation_angle = 180 + r180_comp_vols = [ + comp.volume for comp in self.test_reactor.shapes_and_components] + for r90_vol, r180_vol in zip(r90_comp_vols, r180_comp_vols): + assert r90_vol == pytest.approx(r180_vol * 0.5, rel=0.1) + + def test_rotation_angle_warning(self): + """Checks that the correct warning message is printed when + rotation_angle = 360.""" + + def warning_trigger(): + try: + self.test_reactor.rotation_angle = 360 + self.test_reactor.shapes_and_components + except BaseException: + pass + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + warning_trigger() + assert len(w) == 1 + assert issubclass(w[-1].category, UserWarning) + assert "360 degree rotation may result in a Standard_ConstructionError or AttributeError" in str( + w[-1].message) diff --git a/tests/test_parametric_reactors/test_SegmentedBlanketBallReactor.py b/tests/test_parametric_reactors/test_SegmentedBlanketBallReactor.py new file mode 100644 index 000000000..6cbe20d9c --- /dev/null +++ b/tests/test_parametric_reactors/test_SegmentedBlanketBallReactor.py @@ -0,0 +1,80 @@ + +import unittest + +import paramak + + +class TestSegmentedBlanketBallReactor(unittest.TestCase): + + def setUp(self): + self.test_reactor = paramak.SegmentedBlanketBallReactor( + inner_bore_radial_thickness=10, + inboard_tf_leg_radial_thickness=30, + center_column_shield_radial_thickness=60, + divertor_radial_thickness=150, + inner_plasma_gap_radial_thickness=30, + plasma_radial_thickness=300, + outer_plasma_gap_radial_thickness=30, + firstwall_radial_thickness=20, + blanket_radial_thickness=50, + blanket_rear_wall_radial_thickness=30, + elongation=2, + triangularity=0.55, + number_of_tf_coils=16, + rotation_angle=180, + pf_coil_radial_thicknesses=[50, 50, 50, 50], + pf_coil_vertical_thicknesses=[50, 50, 50, 50], + pf_coil_to_rear_blanket_radial_gap=50, + pf_coil_to_tf_coil_radial_gap=50, + outboard_tf_coil_radial_thickness=100, + outboard_tf_coil_poloidal_thickness=50, + gap_between_blankets=30, + number_of_blanket_segments=4, + ) + + def test_gap_between_blankets_impacts_volume(self): + """Creates a SegmentedBlanketBallReactor with different + gap_between_blankets and checks the volume of the blankes and the + firstwall changes.""" + + self.test_reactor.create_solids() + small_gap_blanket_volume = self.test_reactor._blanket.volume + small_gap_fw_volume = self.test_reactor._firstwall.volume + + self.test_reactor.gap_between_blankets = 60 + self.test_reactor.create_solids() + large_gap_blanket_volume = self.test_reactor._blanket.volume + large_gap_fw_volume = self.test_reactor._firstwall.volume + + assert small_gap_blanket_volume > large_gap_blanket_volume + assert small_gap_fw_volume > large_gap_fw_volume + + def test_number_of_blanket_segments_impacts_volume(self): + """Creates a SegmentedBlanketBallReactor with different + number_of_blanket_segments and checks the volume of the blanket and + firstwall changes.""" + + self.test_reactor.create_solids() + blanket_volume_few_segments = self.test_reactor._blanket.volume + fw_volume_few_segments = self.test_reactor._firstwall.volume + + self.test_reactor.number_of_blanket_segments = 6 + self.test_reactor.create_solids() + blanket_volume_many_segments = self.test_reactor._blanket.volume + fw_volume_many_segments = self.test_reactor._firstwall.volume + + assert blanket_volume_many_segments < blanket_volume_few_segments + assert fw_volume_many_segments > fw_volume_few_segments + + def test_invalid_parameter_error_raises(self): + """Checks that the correct errors are raised when invalid arguments for + parameters are input.""" + + def invalid_gap_between_blankets(): + self.test_reactor.gap_between_blankets = -1 + + def invalid_number_of_blanket_segments(): + self.test_reactor.number_of_blanket_segments = 1 + + self.assertRaises(ValueError, invalid_gap_between_blankets) + self.assertRaises(ValueError, invalid_number_of_blanket_segments) diff --git a/tests/test_parametric_reactors/test_SingleNullBallReactor.py b/tests/test_parametric_reactors/test_SingleNullBallReactor.py new file mode 100644 index 000000000..011549a4a --- /dev/null +++ b/tests/test_parametric_reactors/test_SingleNullBallReactor.py @@ -0,0 +1,102 @@ + +# import unittest + +# import paramak +# import pytest + + +# class test_SingleNullBallReactor(unittest.TestCase): + +# def setUp(self): +# self.test_reactor = paramak.SingleNullBallReactor( +# inner_bore_radial_thickness=10, +# inboard_tf_leg_radial_thickness=30, +# center_column_shield_radial_thickness=60, +# divertor_radial_thickness=50, +# inner_plasma_gap_radial_thickness=30, +# plasma_radial_thickness=300, +# outer_plasma_gap_radial_thickness=30, +# firstwall_radial_thickness=30, +# blanket_radial_thickness=30, +# blanket_rear_wall_radial_thickness=30, +# elongation=2, +# triangularity=0.55, +# number_of_tf_coils=16, +# pf_coil_radial_thicknesses=[50, 50, 50, 50], +# pf_coil_vertical_thicknesses=[50, 50, 50, 50], +# pf_coil_to_rear_blanket_radial_gap=50, +# pf_coil_to_tf_coil_radial_gap=50, +# pf_coil_case_thickness=10, +# outboard_tf_coil_radial_thickness=100, +# outboard_tf_coil_poloidal_thickness=50, +# divertor_position="lower", +# rotation_angle=180, +# ) + +# def test_SingleNullBallReactor_with_pf_and_tf_coils(self): +# """Checks that a SingleNullBallReactor with optional pf and tf coils can +# be created and that the correct number of components are produced.""" + +# assert len(self.test_reactor.shapes_and_components) == 10 + +# def test_SingleNullBallReactor_rotation_angle_impacts_volume(self): +# """Creates SingleNullBallReactors with different rotation angles and checks +# that the relative volumes of the components are correct.""" + +# self.test_reactor.rotation_angle = 90 +# test_reactor_90_components = [ +# component for component in self.test_reactor.shapes_and_components] +# self.test_reactor.rotation_angle = 180 +# test_reactor_180_components = [ +# component for component in self.test_reactor.shapes_and_components] + +# for r90, r180 in zip(test_reactor_90_components, +# test_reactor_180_components): +# assert r90.volume == pytest.approx(r180.volume * 0.5, rel=0.1) + + +# def test_hash_value(self): +# """Creates a single null ball reactor and checks that all shapes in the reactor are created +# when .shapes_and_components is first called. Checks that when .shapes_and_components is +# called again with no changes to the reactor, the shapes in the reactor are not reconstructed +# and the previously constructed shapes are returned. Checks that when .shapes_and_components +# is called again with changes to the reactor, the shapes in the reactor are reconstructed and +# these new shapes are returned. Checks that the reactor_hash_value is only updated when the +# reactor is reconstruced.""" + +# assert self.test_reactor.reactor_hash_value is None +# for key in [ +# "_plasma", +# "_inboard_tf_coils", +# "_center_column_shield", +# "_divertor", +# "_firstwall", +# "_blanket", +# "_blanket_rear_wall", +# "_pf_coil", +# "_pf_coils_casing", +# "_tf_coil" +# ]: +# assert key not in self.test_reactor.__dict__.keys() +# assert self.test_reactor.shapes_and_components is not None + +# for key in [ +# "_plasma", +# "_inboard_tf_coils", +# "_center_column_shield", +# "_divertor", +# "_firstwall", +# "_blanket", +# "_blanket_rear_wall", +# "_pf_coil", +# "_pf_coils_casing", +# "_tf_coil" +# ]: +# assert key in self.test_reactor.__dict__.keys() +# assert len(self.test_reactor.shapes_and_components) == 10 +# assert self.test_reactor.reactor_hash_value is not None +# initial_hash_value = self.test_reactor.reactor_hash_value +# self.test_reactor.rotation_angle = 270 +# assert self.test_reactor.reactor_hash_value == initial_hash_value +# assert self.test_reactor.shapes_and_components is not None +# assert self.test_reactor.reactor_hash_value != initial_hash_value diff --git a/tests/test_parametric_reactors/test_SingleNullSubmersionTokamak.py b/tests/test_parametric_reactors/test_SingleNullSubmersionTokamak.py new file mode 100644 index 000000000..97fb9d3f4 --- /dev/null +++ b/tests/test_parametric_reactors/test_SingleNullSubmersionTokamak.py @@ -0,0 +1,124 @@ + +# import unittest + +# import paramak +# import pytest + + +# class test_SingleNullSubmersionTokamak(unittest.TestCase): + +# def setUp(self): +# self.test_reactor = paramak.SingleNullSubmersionTokamak( +# inner_bore_radial_thickness=10, +# inboard_tf_leg_radial_thickness=30, +# center_column_shield_radial_thickness=60, +# divertor_radial_thickness=50, +# inner_plasma_gap_radial_thickness=30, +# plasma_radial_thickness=300, +# outer_plasma_gap_radial_thickness=30, +# firstwall_radial_thickness=30, +# blanket_rear_wall_radial_thickness=30, +# support_radial_thickness=20, +# inboard_blanket_radial_thickness=20, +# outboard_blanket_radial_thickness=20, +# elongation=2.3, +# triangularity=0.45, +# divertor_position="upper", +# support_position="upper", +# rotation_angle=359, +# ) + +# def test_SingleNullSubmersionTokamak_with_pf_and_tf_coils(self): +# """Creates a SingleNullSubmersionTokamak with pf and tf coils and checks +# that the correct number of components are created.""" + +# self.test_reactor.pf_coil_radial_thicknesses = [50, 50, 50, 50] +# self.test_reactor.pf_coil_vertical_thicknesses = [50, 50, 50, 50] +# self.test_reactor.pf_coil_to_tf_coil_radial_gap = 50 +# self.test_reactor.pf_coil_case_thickness = 10 +# self.test_reactor.outboard_tf_coil_radial_thickness = 100 +# self.test_reactor.outboard_tf_coil_poloidal_thickness = 50 +# self.test_reactor.tf_coil_to_rear_blanket_radial_gap = 20 +# self.test_reactor.number_of_tf_coils = 16 + +# assert len(self.test_reactor.shapes_and_components) == 11 + +# def test_SingleNullSubmersionTokamak_rotation_angle_impacts_volume(self): +# """Creates SingleNullSubmersionTokamaks with different rotation angles and +# checks that the relative volumes of the components are correct.""" + +# self.test_reactor.rotation_angle = 90 +# comps_90_vol = [ +# comp.volume for comp in self.test_reactor.shapes_and_components] +# self.test_reactor.rotation_angle = 180 +# comps_180_vol = [ +# comp.volume for comp in self.test_reactor.shapes_and_components] + +# for vol_90, vol_180 in zip(comps_90_vol, comps_180_vol): +# assert vol_90 == pytest.approx(vol_180 * 0.5, rel=0.1) + +# def test_hash_value(self): +# """Creates a single null submersion reactor and checks that all shapes in the reactor +# are created when .shapes_and_components is first called. Checks that when +# .shapes_and_components is called again with no changes to the reactor, the shapes in +# the reactor are reconstructed and the previously constructed shapes are returned. +# Checks that when .shapes_and_components is called again with changes to the reactor, +# the shapes in the reactor are reconstructed and these new shapes are returned. Checks +# that the reactor_hash_value is only updated when the reactor is +# reconstructed.""" + +# self.test_reactor.pf_coil_radial_thicknesses = [30, 30, 30, 30] +# self.test_reactor.pf_coil_vertical_thicknesses = [30, 30, 30, 30] +# self.test_reactor.pf_coil_to_tf_coil_radial_gap = 50 +# self.test_reactor.pf_coil_case_thickness = 10 +# self.test_reactor.outboard_tf_coil_radial_thickness = 30 +# self.test_reactor.outboard_tf_coil_poloidal_thickness = 30 +# self.test_reactor.tf_coil_to_rear_blanket_radial_gap = 20 +# self.test_reactor.number_of_tf_coils = 16 + +# assert self.test_reactor.reactor_hash_value is None +# for key in [ +# "_inboard_tf_coils", +# "_center_column_shield", +# "_plasma", +# "_inboard_firstwall", +# "_inboard_blanket", +# "_firstwall", +# "_divertor", +# "_blanket", +# "_supports", +# "_outboard_rear_blanket_wall_upper", +# "_outboard_rear_blanket_wall_lower", +# "_outboard_rear_blanket_wall", +# "_tf_coil", +# "_pf_coil", +# "_pf_coils_casing" +# ]: +# assert key not in self.test_reactor.__dict__.keys() +# assert self.test_reactor.shapes_and_components is not None + +# for key in [ +# "_inboard_tf_coils", +# "_center_column_shield", +# "_plasma", +# "_inboard_firstwall", +# "_inboard_blanket", +# "_firstwall", +# "_divertor", +# "_blanket", +# "_supports", +# "_outboard_rear_blanket_wall_upper", +# "_outboard_rear_blanket_wall_lower", +# "_outboard_rear_blanket_wall", +# "_tf_coil", +# "_pf_coil", +# "_pf_coils_casing" +# ]: +# assert key in self.test_reactor.__dict__.keys() +# assert len(self.test_reactor.shapes_and_components) == 11 +# assert self.test_reactor.reactor_hash_value is not None +# initial_hash_value = self.test_reactor.reactor_hash_value +# self.test_reactor.rotation_angle = 270 +# assert self.test_reactor.reactor_hash_value == initial_hash_value +# assert self.test_reactor.shapes_and_components is not None +# assert self.test_reactor.reactor_hash_value != initial_hash_value diff --git a/tests/test_parametric_reactors/test_SubmersionTokamak.py b/tests/test_parametric_reactors/test_SubmersionTokamak.py new file mode 100644 index 000000000..3c4fc3e9a --- /dev/null +++ b/tests/test_parametric_reactors/test_SubmersionTokamak.py @@ -0,0 +1,273 @@ + +import os +import unittest +from pathlib import Path + +import paramak +import pytest + + +class TestSubmersionTokamak(unittest.TestCase): + + def setUp(self): + self.test_reactor = paramak.SubmersionTokamak( + inner_bore_radial_thickness=10, + inboard_tf_leg_radial_thickness=30, + center_column_shield_radial_thickness=60, + divertor_radial_thickness=50, + inner_plasma_gap_radial_thickness=30, + plasma_radial_thickness=300, + outer_plasma_gap_radial_thickness=30, + firstwall_radial_thickness=30, + blanket_rear_wall_radial_thickness=30, + number_of_tf_coils=16, + support_radial_thickness=20, + inboard_blanket_radial_thickness=20, + outboard_blanket_radial_thickness=20, + elongation=2.3, + triangularity=0.45, + rotation_angle=359, + ) + + def test_svg_creation(self): + """Creates a SubmersionTokamak and checks that an svg file of the reactor + can be exported using the export_svg method.""" + + os.system("rm test_image.svg") + self.test_reactor.export_svg("test_image.svg") + + assert Path("test_image.svg").exists() is True + os.system("rm test_image.svg") + + def test_minimal_creation(self): + """Creates a SubmersionTokamak and checks that the correct number of + components are created.""" + + assert len(self.test_reactor.shapes_and_components) == 8 + + def test_with_tf_coils_creation(self): + """Creates a SubmersionTokamak with tf coils and checks that the correct + number of components are created.""" + + self.test_reactor.outboard_tf_coil_radial_thickness = 50 + self.test_reactor.tf_coil_to_rear_blanket_radial_gap = 50 + self.test_reactor.outboard_tf_coil_poloidal_thickness = 70 + self.test_reactor.number_of_tf_coils = 4 + + assert len(self.test_reactor.shapes_and_components) == 9 + + def test_with_tf_and_pf_coils_creation(self): + """Creates a SubmersionTokamak with tf and pf coils and checks that the + correct number of components are created.""" + + self.test_reactor.outboard_tf_coil_radial_thickness = 50 + self.test_reactor.tf_coil_to_rear_blanket_radial_gap = 50 + self.test_reactor.outboard_tf_coil_poloidal_thickness = 70 + self.test_reactor.pf_coil_vertical_thicknesses = [50, 50, 50, 50, 50] + self.test_reactor.pf_coil_radial_thicknesses = [40, 40, 40, 40, 40] + self.test_reactor.pf_coil_to_tf_coil_radial_gap = 50 + self.test_reactor.pf_coil_case_thickness = 10 + self.test_reactor.number_of_tf_coils = 4 + + assert len(self.test_reactor.shapes_and_components) == 11 + + def test_minimal_stp_creation(self): + """Creates a SubmersionTokamak and checks that stp files of all components + can be exported using the export_stp method.""" + + os.system("rm -r minimal_SubmersionTokamak") + self.test_reactor.export_stp("minimal_SubmersionTokamak") + + output_filenames = [ + "minimal_SubmersionTokamak/inboard_tf_coils.stp", + "minimal_SubmersionTokamak/center_column_shield.stp", + "minimal_SubmersionTokamak/plasma.stp", + "minimal_SubmersionTokamak/divertor.stp", + "minimal_SubmersionTokamak/outboard_firstwall.stp", + "minimal_SubmersionTokamak/supports.stp", + "minimal_SubmersionTokamak/blanket.stp", + "minimal_SubmersionTokamak/outboard_rear_blanket_wall.stp", + "minimal_SubmersionTokamak/Graveyard.stp", + ] + + for output_filename in output_filenames: + assert Path(output_filename).exists() is True + os.system("rm -r minimal_SubmersionTokamak") + + def test_with_pf_coils_stp_creation(self): + """Creates a SubmersionTokamak with pf coils and checks that stp files + of all components can be exported using the export_stp method.""" + + os.system("rm -r pf_SubmersionTokamak") + + self.test_reactor.outboard_tf_coil_radial_thickness = 50 + self.test_reactor.tf_coil_to_rear_blanket_radial_gap = 50 + self.test_reactor.outboard_tf_coil_poloidal_thickness = 70 + self.test_reactor.number_of_tf_coils = 4 + + self.test_reactor.export_stp("pf_SubmersionTokamak") + + output_filenames = [ + "pf_SubmersionTokamak/inboard_tf_coils.stp", + "pf_SubmersionTokamak/center_column_shield.stp", + "pf_SubmersionTokamak/plasma.stp", + "pf_SubmersionTokamak/divertor.stp", + "pf_SubmersionTokamak/outboard_firstwall.stp", + "pf_SubmersionTokamak/supports.stp", + "pf_SubmersionTokamak/blanket.stp", + "pf_SubmersionTokamak/outboard_rear_blanket_wall.stp", + "pf_SubmersionTokamak/Graveyard.stp", + ] + + for output_filename in output_filenames: + assert Path(output_filename).exists() is True + os.system("rm -r pf_SubmersionTokamak") + + def test_with_tf_and_pf_coils_stp_creation(self): + """Creates a SubmersionTokamak with tf and pf coils and checks that + stp files of all components can be exported using the export_stp method.""" + + os.system("rm -r tf_pf_SubmersionTokamak") + + self.test_reactor.outboard_tf_coil_radial_thickness = 50 + self.test_reactor.tf_coil_to_rear_blanket_radial_gap = 50 + self.test_reactor.outboard_tf_coil_poloidal_thickness = 70 + self.test_reactor.pf_coil_vertical_thicknesses = [50, 50, 50, 50, 50] + self.test_reactor.pf_coil_radial_thicknesses = [40, 40, 40, 40, 40] + self.test_reactor.pf_coil_to_tf_coil_radial_gap = 50 + self.test_reactor.pf_coil_case_thickness = 10 + self.test_reactor.number_of_tf_coils = 4 + + self.test_reactor.export_stp("tf_pf_SubmersionTokamak") + + output_filenames = [ + "tf_pf_SubmersionTokamak/inboard_tf_coils.stp", + "tf_pf_SubmersionTokamak/center_column_shield.stp", + "tf_pf_SubmersionTokamak/plasma.stp", + "tf_pf_SubmersionTokamak/divertor.stp", + "tf_pf_SubmersionTokamak/outboard_firstwall.stp", + "tf_pf_SubmersionTokamak/supports.stp", + "tf_pf_SubmersionTokamak/blanket.stp", + "tf_pf_SubmersionTokamak/outboard_rear_blanket_wall.stp", + "tf_pf_SubmersionTokamak/Graveyard.stp", + "tf_pf_SubmersionTokamak/pf_coil_cases.stp" + ] + + for output_filename in output_filenames: + assert Path(output_filename).exists() is True + os.system("rm -r tf_pf_SubmersionTokamak") + + def test_rotation_angle_warning(self): + """Creates a SubmersionTokamak with rotation_angle = 360 and checks that the + correct warning message is printed.""" + + def warning_trigger(): + try: + self.test_reactor.rotation_angle = 360 + self.test_reactor._rotation_angle_check() + except BaseException: + pass + msg = "360 degree rotation may result in a " + \ + "Standard_ConstructionError or AttributeError" + with pytest.warns(UserWarning, match=msg): + warning_trigger() + + def test_submersion_reactor_hash_value(self): + """Creates a submersion reactor and checks that all shapes in the reactor are created + when .shapes_and_components is first called. Checks that when .shapes_and_components + is called again with no changes to the reactor, the shapes in the reactor are + reconstructed and the previously constructed shapes are returned. Checks that when + .shapes_and_components is called again with no changes to the reactor, the shapes in + the reactor are reconstructed and these new shapes are returned. Checks that the + reactor_hash_value is only updated when the reactor is reconstructed.""" + + self.test_reactor.pf_coil_radial_thicknesses = [30, 30, 30, 30] + self.test_reactor.pf_coil_vertical_thicknesses = [30, 30, 30, 30] + self.test_reactor.pf_coil_to_tf_coil_radial_gap = 50 + self.test_reactor.pf_coil_case_thickness = 10 + self.test_reactor.outboard_tf_coil_radial_thickness = 30 + self.test_reactor.outboard_tf_coil_poloidal_thickness = 30 + self.test_reactor.tf_coil_to_rear_blanket_radial_gap = 20 + self.test_reactor.number_of_tf_coils = 16 + + assert self.test_reactor.reactor_hash_value is None + for key in [ + "_inboard_tf_coils", + "_center_column_shield", + "_plasma", + "_inboard_firstwall", + "_inboard_blanket", + "_firstwall", + "_divertor", + "_blanket", + "_supports", + "_outboard_rear_blanket_wall_upper", + "_outboard_rear_blanket_wall_lower", + "_outboard_rear_blanket_wall", + "_tf_coil", + "_pf_coil", + "_pf_coils_casing" + ]: + assert key not in self.test_reactor.__dict__.keys() + + assert self.test_reactor.shapes_and_components is not None + for key in [ + "_inboard_tf_coils", + "_center_column_shield", + "_plasma", + "_inboard_firstwall", + "_inboard_blanket", + "_firstwall", + "_divertor", + "_blanket", + "_supports", + "_outboard_rear_blanket_wall_upper", + "_outboard_rear_blanket_wall_lower", + "_outboard_rear_blanket_wall", + "_tf_coil", + "_pf_coil", + "_pf_coils_casing" + ]: + assert key in self.test_reactor.__dict__.keys() + + assert len(self.test_reactor.shapes_and_components) == 11 + assert self.test_reactor.reactor_hash_value is not None + initial_hash_value = self.test_reactor.reactor_hash_value + self.test_reactor.rotation_angle = 270 + assert self.test_reactor.reactor_hash_value == initial_hash_value + assert self.test_reactor.shapes_and_components is not None + assert self.test_reactor.reactor_hash_value != initial_hash_value + + def test_error_divertor_pos(self): + """Checks an invalid divertor and support + position raises the correct ValueError.""" + + def invalid_divertor_position(): + self.test_reactor.divertor_position = "coucou" + + self.assertRaises(ValueError, invalid_divertor_position) + + def invalid_support_position(): + self.test_reactor.support_position = "coucou" + + self.assertRaises(ValueError, invalid_support_position) + + def test_divertors_supports(self): + """Checks that SubmersionTokamaks with lower and upper supports + and divertors can be created.""" + + self.test_reactor.divertor_position = "lower" + self.test_reactor.support_position = "lower" + assert self.test_reactor.solid is not None + + self.test_reactor.divertor_position = "lower" + self.test_reactor.support_position = "upper" + assert self.test_reactor.solid is not None + + self.test_reactor.divertor_position = "upper" + self.test_reactor.support_position = "lower" + assert self.test_reactor.solid is not None + + self.test_reactor.divertor_position = "upper" + self.test_reactor.support_position = "upper" + assert self.test_reactor.solid is not None diff --git a/tests/test_parametric_shapes/__init__.py b/tests/test_parametric_shapes/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_parametric_shapes/test_ExtrudeCircleShape.py b/tests/test_parametric_shapes/test_ExtrudeCircleShape.py new file mode 100644 index 000000000..3c8e99f99 --- /dev/null +++ b/tests/test_parametric_shapes/test_ExtrudeCircleShape.py @@ -0,0 +1,98 @@ +import math +import unittest + +import pytest + +from paramak import ExtrudeCircleShape + + +class TestExtrudeCircleShape(unittest.TestCase): + + def setUp(self): + self.test_shape = ExtrudeCircleShape( + points=[(30, 0)], radius=10, distance=30 + ) + + def test_default_parameters(self): + """Checks that the default parameters of an ExtrudeCircleShape are correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.stp_filename == "ExtrudeCircleShape.stp" + assert self.test_shape.stl_filename == "ExtrudeCircleShape.stl" + assert self.test_shape.extrude_both + + def test_absolute_shape_volume(self): + """Creates an ExtrudeCircleShape and checks that its volume is correct.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume == pytest.approx(math.pi * (10**2) * 30) + + def test_relative_shape_volume(self): + """Creates two ExtrudeCircleShapes and checks that their relative volumes + are correct.""" + + test_volume = self.test_shape.volume + self.test_shape.azimuth_placement_angle = [0, 90, 180, 270] + assert test_volume * 4 == pytest.approx(self.test_shape.volume) + + def test_absolute_shape_areas(self): + """Creates ExtrudeCircleShapes and checks that the areas of each face + are correct.""" + + assert self.test_shape.area == pytest.approx( + (math.pi * (10**2) * 2) + (math.pi * (2 * 10) * 30) + ) + assert len(self.test_shape.areas) == 3 + assert self.test_shape.areas.count( + pytest.approx(math.pi * (10**2))) == 2 + assert self.test_shape.areas.count( + pytest.approx(math.pi * (2 * 10) * 30)) == 1 + + def test_cut_volume(self): + """Creates an ExtrudeCircleShape with another ExtrudeCircleShape cut out + and checks that the volume is correct.""" + + shape_with_cut = ExtrudeCircleShape( + points=[(30, 0)], radius=20, distance=40, + cut=self.test_shape + ) + + assert shape_with_cut.volume == pytest.approx( + (math.pi * (20**2) * 40) - (math.pi * (10**2) * 30) + ) + + def test_intersect_volume(self): + """Creates ExtrudeCircleShapes with other ExtrudeCircleShapes intersected + and checks that their volumes are correct.""" + + intersect_shape = ExtrudeCircleShape( + points=[(30, 0)], radius=5, distance=50) + + intersected_shape = ExtrudeCircleShape( + points=[(30, 0)], radius=10, distance=50, + intersect=[self.test_shape, intersect_shape] + ) + + assert intersected_shape.volume == pytest.approx(math.pi * 5**2 * 30) + + def test_rotation_angle(self): + """Creates an ExtrudeCircleShape with a rotation_angle < 360 and checks that + the correct cut is performed and the volume is correct.""" + + self.test_shape.azimuth_placement_angle = [45, 135, 225, 315] + test_volume = self.test_shape.volume + self.test_shape.rotation_angle = 180 + assert self.test_shape.volume == pytest.approx(test_volume * 0.5) + + def test_extrude_both(self): + """Creates an ExtrudeCircleShape with extrude_both = True and False and checks + that the volumes are correct.""" + + test_volume_extrude_both = self.test_shape.volume + self.test_shape.extrude_both = False + assert self.test_shape.volume == pytest.approx( + test_volume_extrude_both) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_parametric_shapes/test_ExtrudeMixedShape.py b/tests/test_parametric_shapes/test_ExtrudeMixedShape.py new file mode 100644 index 000000000..8d090b6d6 --- /dev/null +++ b/tests/test_parametric_shapes/test_ExtrudeMixedShape.py @@ -0,0 +1,135 @@ + +import os +import unittest +from pathlib import Path + +import pytest +from paramak import ExtrudeMixedShape + + +class TestExtrudeMixedShape(unittest.TestCase): + + def setUp(self): + self.test_shape = ExtrudeMixedShape( + points=[(50, 0, "straight"), (50, 50, "spline"), (60, 70, "spline"), + (70, 50, "circle"), (60, 25, "circle"), (70, 0, "straight")], + distance=50 + ) + + def test_default_parameters(self): + """Checks that the default parameters of an ExtrudeMixedShape are correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.stp_filename == "ExtrudeMixedShape.stp" + assert self.test_shape.stl_filename == "ExtrudeMixedShape.stl" + assert self.test_shape.azimuth_placement_angle == 0 + + def test_absolute_shape_volume(self): + """Creates an ExtrudeMixedShape and checks that the volume is correct.""" + + assert self.test_shape.volume > 20 * 20 * 30 + + def test_relative_shape_volume(self): + """Creates two ExtrudeMixedShapes and checks that their relative volumes + are correct.""" + + test_volume = self.test_shape.volume + self.test_shape.azimuth_placement_angle = [0, 180] + + assert self.test_shape.volume == pytest.approx( + test_volume * 2, rel=0.01) + + def test_shape_face_areas(self): + """Creates an ExtrudeMixedShape and checks that the face areas are expected.""" + + self.test_shape.extrude_both = False + assert len(self.test_shape.areas) == 6 + assert len(set([round(i) for i in self.test_shape.areas])) == 5 + + def test_cut_volume(self): + """Creates an ExtrudeMixedShape with another ExtrudeMixedShape cut out and + checks that the volume is correct.""" + + inner_shape = ExtrudeMixedShape( + points=[ + (5, 5, "straight"), + (5, 10, "spline"), + (10, 10, "spline"), + (10, 5, "spline"), + ], + distance=30, + ) + + outer_shape = ExtrudeMixedShape( + points=[ + (3, 3, "straight"), + (3, 12, "spline"), + (12, 12, "spline"), + (12, 3, "spline"), + ], + distance=30, + ) + + outer_shape_with_cut = ExtrudeMixedShape( + points=[ + (3, 3, "straight"), + (3, 12, "spline"), + (12, 12, "spline"), + (12, 3, "spline"), + ], + cut=inner_shape, + distance=30, + ) + + assert inner_shape.volume == pytest.approx(1068, abs=2) + assert outer_shape.volume == pytest.approx(3462, abs=2) + assert outer_shape_with_cut.volume == pytest.approx(3462 - 1068, abs=2) + + def test_export_stp(self): + """Creates an ExtrudeMixedShape and checks that a stp file of the shape + can be exported with the correct suffix using the export_stp method.""" + + os.system("rm filename.stp filename.step") + self.test_shape.export_stp("filename.stp") + self.test_shape.export_stp("filename.step") + assert Path("filename.stp").exists() is True + assert Path("filename.step").exists() is True + os.system("rm filename.stp filename.step") + self.test_shape.export_stp("filename") + assert Path("filename.stp").exists() is True + os.system("rm filename.stp") + + def test_export_stl(self): + """Creates a ExtrudeMixedShape and checks that a stl file of the shape + can be exported with the correct suffix using the export_stl method.""" + + os.system("rm filename.stl") + self.test_shape.export_stl("filename.stl") + assert Path("filename.stl").exists() is True + os.system("rm filename.stl") + self.test_shape.export_stl("filename") + assert Path("filename.stl").exists() is True + os.system("rm filename.stl") + + def test_rotation_angle(self): + """Creates an ExtrudeMixedShape with a rotation_angle < 360 and checks that + the correct cut is performed and the volume is correct.""" + + self.test_shape.azimuth_placement_angle = [45, 135, 225, 315] + test_volume = self.test_shape.volume + self.test_shape.rotation_angle = 180 + assert self.test_shape.volume == pytest.approx( + test_volume * 0.5, rel=0.01) + + def test_extrude_both(self): + """Creates an ExtrudeMixedShape with extrude_both = True and False and checks + that the volumes are correct.""" + + test_volume_extrude_both = self.test_shape.volume + self.test_shape.extrude_both = False + assert self.test_shape.volume == pytest.approx( + test_volume_extrude_both) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_parametric_shapes/test_ExtrudeSplineShape.py b/tests/test_parametric_shapes/test_ExtrudeSplineShape.py new file mode 100644 index 000000000..acafc6606 --- /dev/null +++ b/tests/test_parametric_shapes/test_ExtrudeSplineShape.py @@ -0,0 +1,89 @@ + +import unittest + +import pytest +from paramak import ExtrudeSplineShape + + +class TestExtrudeSplineShape(unittest.TestCase): + + def setUp(self): + self.test_shape = ExtrudeSplineShape( + points=[(50, 0), (50, 20), (70, 80), (90, 50), (70, 0), (90, -50), + (70, -80), (50, -20)], distance=30 + ) + + def test_default_parameters(self): + """Checks that the default parameters of an ExtrudeSplineShape are correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.stp_filename == "ExtrudeSplineShape.stp" + assert self.test_shape.stl_filename == "ExtrudeSplineShape.stl" + assert self.test_shape.extrude_both + + def test_absolute_shape_volume(self): + """Creates an ExtrudeSplineShape and checks that the volume is correct.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 20 * 20 * 30 + + def test_shape_face_areas(self): + """Creates an ExtrudeSplineShape and checks that the face areas are expected.""" + + self.test_shape.extrude_both = False + assert len(self.test_shape.areas) == 3 + assert len(set([round(i) for i in self.test_shape.areas])) == 2 + + def test_relative_shape_volume(self): + """Creates two ExtrudeSplineShapes and checks that their relative volumes + are correct.""" + + test_volume = self.test_shape.volume + self.test_shape.azimuth_placement_angle = [0, 180] + + assert self.test_shape.volume == pytest.approx( + test_volume * 2, rel=0.01) + + def test_cut_volume(self): + """Creates an ExtrudeSplineShape with another ExtrudeSplineShape cut out + and checks that the volume is correct.""" + + inner_shape = ExtrudeSplineShape( + points=[(5, 5), (5, 10), (10, 10), (10, 5)], distance=30 + ) + + outer_shape = ExtrudeSplineShape( + points=[(3, 3), (3, 12), (12, 12), (12, 3)], distance=30 + ) + + outer_shape_with_cut = ExtrudeSplineShape( + points=[(3, 3), (3, 12), (12, 12), (12, 3)], cut=inner_shape, + distance=30, + ) + + assert inner_shape.volume == pytest.approx(1165, abs=2) + assert outer_shape.volume == pytest.approx(3775, abs=2) + assert outer_shape_with_cut.volume == pytest.approx(3775 - 1165, abs=2) + + def test_rotation_angle(self): + """Creates an ExtrudeStraightShape with a rotation_angle < 360 and checks that + the correct cut is performed and the volume is correct.""" + + self.test_shape.azimuth_placement_angle = [45, 135, 225, 315] + test_volume = self.test_shape.volume + self.test_shape.rotation_angle = 180 + assert self.test_shape.volume == pytest.approx( + test_volume * 0.5, rel=0.01) + + def test_extrude_both(self): + """Creates an ExtrudeSplineShape with extrude_both = True and False and checks + that the volumes are correct.""" + + test_volume_extrude_both = self.test_shape.volume + self.test_shape.extrude_both = False + assert self.test_shape.volume == pytest.approx( + test_volume_extrude_both) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_parametric_shapes/test_ExtrudeStraightShape.py b/tests/test_parametric_shapes/test_ExtrudeStraightShape.py new file mode 100644 index 000000000..1a52536d0 --- /dev/null +++ b/tests/test_parametric_shapes/test_ExtrudeStraightShape.py @@ -0,0 +1,103 @@ + +import unittest + +import pytest +from paramak import ExtrudeStraightShape + + +class TestExtrudeStraightShape(unittest.TestCase): + + def setUp(self): + self.test_shape = ExtrudeStraightShape( + points=[(10, 10), (10, 30), (30, 30), (30, 10)], distance=30 + ) + + def test_default_parameters(self): + """Checks that the default parameters of an ExtrudeStraightShape are correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.stp_filename == "ExtrudeStraightShape.stp" + assert self.test_shape.stl_filename == "ExtrudeStraightShape.stl" + assert self.test_shape.extrude_both + + def test_absolute_shape_volume(self): + """Creates an ExtrudeStraightShape and checks that the volume is correct.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume == pytest.approx(20 * 20 * 30) + + def test_absolute_shape_areas(self): + """Creates an ExtrudeStraightShape and checks that the volume is correct.""" + + assert self.test_shape.area == pytest.approx( + (20 * 20 * 2) + (20 * 30 * 4) + ) + assert len(self.test_shape.areas) == 6 + assert self.test_shape.areas.count(pytest.approx(20 * 20)) == 2 + assert self.test_shape.areas.count(pytest.approx(20 * 30)) == 4 + + def test_relative_shape_volume(self): + """Creates two ExtrudeStraightShapes and checks that their relative volumes + are correct.""" + + test_volume = self.test_shape.volume + self.test_shape.azimuth_placement_angle = [0, 90, 180, 270] + self.test_shape.rotation_axis = "Y" + assert self.test_shape.volume == pytest.approx(test_volume * 4) + + def test_cut_volume(self): + """Creates an ExtrudeStraightShape with another ExtrudeStraightShape cut out + and checks that the volume is correct.""" + + shape_with_cut = ExtrudeStraightShape( + points=[(0, 0), (0, 40), (40, 40), (40, 0)], distance=40, + cut=self.test_shape + ) + + assert shape_with_cut.volume == pytest.approx( + (40 * 40 * 40) - (20 * 20 * 30) + ) + + def test_union_volume(self): + """Creates a union of two ExtrudeStraightShapes and checks that the volume is + correct.""" + + unioned_shape = ExtrudeStraightShape( + points=[(0, 10), (0, 30), (20, 30), (20, 10)], distance=30, + union=self.test_shape + ) + assert unioned_shape.volume == pytest.approx(30 * 20 * 30) + + def test_intersect_volume(self): + """Creates an ExtrudeStraightShape with another ExtrudeStraightShape intersected + and checks that the volume is correct.""" + + intersected_shape = ExtrudeStraightShape( + points=[(0, 10), (0, 30), (20, 30), (20, 10)], distance=30, + intersect=self.test_shape + ) + assert intersected_shape.volume == pytest.approx(10 * 20 * 30) + + def test_rotation_angle(self): + """Creates an ExtrudeStraightShape with a rotation_angle < 360 and checks that + the correct cut is performed and the volume is correct.""" + + self.test_shape.azimuth_placement_angle = [45, 135, 225, 315] + self.rotation_axis = "Y" + test_volume = self.test_shape.volume + self.test_shape.rotation_angle = 180 + assert self.test_shape.volume == pytest.approx( + test_volume * 0.5, rel=0.01) + + def test_extrude_both(self): + """Creates an ExtrudeStraightShape with extrude_both = True and False and checks + that the volumes are correct.""" + + test_volume_extrude_both = self.test_shape.volume + self.test_shape.extrude_both = False + assert self.test_shape.volume == pytest.approx( + test_volume_extrude_both) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_parametric_shapes/test_RotateCircleShape.py b/tests/test_parametric_shapes/test_RotateCircleShape.py new file mode 100644 index 000000000..a27c0c8dd --- /dev/null +++ b/tests/test_parametric_shapes/test_RotateCircleShape.py @@ -0,0 +1,86 @@ + +import math +import unittest + +import pytest +from paramak import RotateCircleShape + + +class TestRotateCircleShape(unittest.TestCase): + + def setUp(self): + self.test_shape = RotateCircleShape( + points=[(60, 0)], + radius=10 + ) + + def test_default_parameters(self): + """Checks that the default parameters of a RotateCircleShape are correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.stp_filename == "RotateCircleShape.stp" + assert self.test_shape.stl_filename == "RotateCircleShape.stl" + assert self.test_shape.azimuth_placement_angle == 0 + + def test_absolute_shape_volume(self): + """Creates RotateCircleShapes and checks that their volumes are correct.""" + + # See Issue #445 + # assert self.test_shape.volume == pytest.approx( + # 2 * math.pi * 60 * math.pi * (10**2) + # ) + self.test_shape.rotation_angle = 270 + assert self.test_shape.volume == pytest.approx( + 2 * math.pi * 60 * math.pi * (10**2) * 0.75 + ) + + def test_absolute_shape_areas(self): + """Creates RotateCircleShapes and checks that the areas of each face are + correct.""" + + # See Issue #445 + # assert self.test_shape.area == pytest.approx( + # math.pi * (10 * 2) * math.pi * (60 * 2)) + # assert len(self.test_shape.areas) == 1 + # assert self.test_shape.areas.count(pytest.approx( + # math.pi * (10 * 2) * math.pi * (60 * 2), rel=0.01)) == 1 + + self.test_shape.rotation_angle = 180 + assert self.test_shape.area == pytest.approx( + ((math.pi * (10**2)) * 2) + (math.pi * (10 * 2) * math.pi * (60 * 2) / 2), rel=0.01) + assert len(self.test_shape.areas) == 3 + assert self.test_shape.areas.count( + pytest.approx(math.pi * (10**2))) == 2 + assert self.test_shape.areas.count(pytest.approx( + math.pi * (10 * 2) * math.pi * (60 * 2) / 2, rel=0.01)) == 1 + + def test_relative_shape_volume_azimuth_placement_angle(self): + """Creates two RotateCircleShapes with different azimuth_placement_angles and + checks that their relative volumes are correct.""" + + self.test_shape.rotation_angle = 10 + assert self.test_shape.volume == pytest.approx( + (math.pi * 10**2) * ((2 * math.pi * 60) / 36) + ) + + self.test_shape.azimuth_placement_angle = [0, 90, 180, 270] + assert self.test_shape.volume == pytest.approx( + (math.pi * 10**2) * ((2 * math.pi * 60) / 36) * 4 + ) + + def test_cut_volume(self): + """Creates a RotateCircleShape with another RotateCircleShape cut out and + checks that the volume is correct.""" + + outer_shape = RotateCircleShape( + points=[(60, 0)], radius=15 + ) + outer_shape_volume = outer_shape.volume + outer_shape.cut = self.test_shape + assert outer_shape.volume == pytest.approx( + outer_shape_volume - self.test_shape.volume + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_parametric_shapes/test_RotateMixedShape.py b/tests/test_parametric_shapes/test_RotateMixedShape.py new file mode 100644 index 000000000..abbde273d --- /dev/null +++ b/tests/test_parametric_shapes/test_RotateMixedShape.py @@ -0,0 +1,186 @@ + +import os +import unittest +from pathlib import Path + +import pytest +from paramak import RotateMixedShape + + +class TestRotateMixedShape(unittest.TestCase): + + def setUp(self): + self.test_shape = RotateMixedShape( + points=[(50, 0, "straight"), (50, 50, "spline"), (60, 70, "spline"), + (70, 50, "circle"), (60, 25, "circle"), (70, 0, "straight")] + ) + + def test_default_parameters(self): + """Checks that the default parameters of a RotateMixedShape are correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.stp_filename == "RotateMixedShape.stp" + assert self.test_shape.stl_filename == "RotateMixedShape.stl" + assert self.test_shape.azimuth_placement_angle == 0 + + def test_relative_shape_volume_rotation_angle(self): + """Creates two RotateMixedShapes with different rotation_angles and checks + that their relative volumes are correct.""" + + assert self.test_shape.volume > 100 + test_volume = self.test_shape.volume + self.test_shape.rotation_angle = 180 + assert test_volume == pytest.approx(self.test_shape.volume * 2) + + def test_relative_shape_volume_azimuth_placement_angle(self): + """Creates two RotateMixedShapes with different azimuth_placement_angles and + checks that their relative volumes are correct.""" + + self.test_shape.rotation_angle = 10 + self.test_shape.azimuth_placement_angle = 0 + test_volume_1 = self.test_shape.volume + + self.test_shape.azimuth_placement_angle = [0, 90, 180, 270] + assert self.test_shape.volume == pytest.approx(test_volume_1 * 4) + + def test_shape_face_areas(self): + """Creates RotateMixedShapes and checks that the face areas are expected.""" + + assert len(self.test_shape.areas) == 4 + assert len(set(self.test_shape.areas)) == 4 + + self.test_shape.rotation_angle = 180 + assert len(self.test_shape.areas) == 6 + assert len(set([round(i) for i in self.test_shape.areas])) == 5 + + def test_union_volume_addition(self): + """Fuses two RotateMixedShapes and checks that their fused volume + is correct""" + + inner_box = RotateMixedShape( + points=[ + (100, 100, "straight"), + (100, 200, "straight"), + (200, 200, "straight"), + (200, 100, "straight"), + ] + ) + + outer_box = RotateMixedShape( + points=[ + (200, 100, "straight"), + (200, 200, "straight"), + (500, 200, "straight"), + (500, 100, "straight"), + ] + ) + + outer_box_and_inner_box = RotateMixedShape( + points=[ + (200, 100, "straight"), + (200, 200, "straight"), + (500, 200, "straight"), + (500, 100, "straight"), + ], + union=inner_box, + ) + + assert inner_box.volume + outer_box.volume == pytest.approx( + outer_box_and_inner_box.volume + ) + + def test_incorrect_connections(self): + """Checks that errors are raised when invalid connection arguments are + used to construct a RotateMixedShape.""" + + def incorrect_string_for_connection_type(): + """Checks ValueError is raised when an invalid connection type is + specified.""" + + RotateMixedShape( + points=[ + (0, 0, "straight"), + (0, 20, "spline"), + (20, 20, "spline"), + (20, 0, "invalid_entry"), + ] + ) + + self.assertRaises(ValueError, incorrect_string_for_connection_type) + + def incorrect_number_of_connections_function(): + """Checks ValueError is raised when an incorrect number of + connections is specified.""" + + test_shape = RotateMixedShape( + points=[(0, 200, "straight"), (200, 100), (0, 0, "spline")] + ) + + self.assertRaises(ValueError, incorrect_number_of_connections_function) + + def test_cut_volume(self): + """Creates a RotateMixedShape with another RotateMixedShape cut out and + checks that the volume is correct.""" + + outer_shape = RotateMixedShape( + points=[ + (40, -10, "spline"), + (35, 50, "spline"), + (60, 80, "straight"), + (80, 80, "circle"), + (100, 40, "circle"), + (80, 0, "straight"), + (80, -10, "straight") + ] + ) + outer_shape_volume = outer_shape.volume + outer_shape.cut = self.test_shape + assert outer_shape.volume == pytest.approx( + outer_shape_volume - self.test_shape.volume + ) + + def test_mixed_shape_with_straight_and_circle(self): + """Creates a RotateMixedShape with straight and circular connections and + checks that the volume is correct.""" + + test_shape = RotateMixedShape( + points=[ + (10, 20, "straight"), + (10, 10, "straight"), + (20, 10, "circle"), + (40, 15, "circle"), + (20, 20, "straight"), + ], + rotation_angle=10, + ) + assert test_shape.volume > 10 * 10 + + def test_export_stp(self): + """Creates a RotateMixedShape and checks that a stp file of the shape + can be exported with the correct suffix using the export_stp method.""" + + os.system("rm filename.stp filename.step") + self.test_shape.export_stp("filename.stp") + self.test_shape.export_stp("filename.step") + assert Path("filename.stp").exists() is True + assert Path("filename.step").exists() is True + os.system("rm filename.stp filename.step") + self.test_shape.export_stp("filename") + assert Path("filename.stp").exists() is True + os.system("rm filename.stp") + + def test_export_stl(self): + """Creates a RotateMixedShape and checks that a stl file of the shape + can be exported with the correct suffix using the export_stl method.""" + + os.system("rm filename.stl") + self.test_shape.export_stl("filename.stl") + assert Path("filename.stl").exists() is True + os.system("rm filename.stl") + self.test_shape.export_stl("filename") + assert Path("filename.stl").exists() is True + os.system("rm filename.stl") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_parametric_shapes/test_RotateSplineShape.py b/tests/test_parametric_shapes/test_RotateSplineShape.py new file mode 100644 index 000000000..db2958685 --- /dev/null +++ b/tests/test_parametric_shapes/test_RotateSplineShape.py @@ -0,0 +1,72 @@ + +import unittest + +import pytest +from paramak import RotateSplineShape + + +class TestRotateSplineShape(unittest.TestCase): + + def setUp(self): + self.test_shape = RotateSplineShape( + points=[(50, 0), (50, 20), (70, 80), (90, 50), (70, 0), + (90, -50), (70, -80), (50, -20)]) + + def test_default_parameters(self): + """Checks that the default parameters of a RotateSplineShape are correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.stp_filename == "RotateSplineShape.stp" + assert self.test_shape.stl_filename == "RotateSplineShape.stl" + assert self.test_shape.azimuth_placement_angle == 0 + + def test_absolute_shape_volume(self): + """creates a rotated shape using spline connections and checks the volume + is correct""" + + self.test_shape.rotation_angle = 360 + volume_360 = self.test_shape.volume + + assert self.test_shape.solid is not None + assert volume_360 > 100 + + self.test_shape.rotation_angle = 180 + assert self.test_shape.solid is not None + assert self.test_shape.volume == pytest.approx(volume_360 * 0.5) + + def test_cut_volume(self): + """Creates a RotateSplineShape with another RotateSplineShape cut out + and checks that the volume is correct.""" + + inner_shape = RotateSplineShape( + points=[(5, 5), (5, 10), (10, 10), (10, 5)], rotation_angle=180 + ) + + outer_shape = RotateSplineShape( + points=[(3, 3), (3, 12), (12, 12), (12, 3)], rotation_angle=180 + ) + + outer_shape_with_cut = RotateSplineShape( + points=[(3, 3), (3, 12), (12, 12), (12, 3)], + cut=inner_shape, + rotation_angle=180, + ) + + assert inner_shape.volume == pytest.approx(900.88, abs=0.1) + assert outer_shape.volume == pytest.approx(2881.76, abs=0.1) + assert outer_shape_with_cut.volume == pytest.approx( + 2881.76 - 900.88, abs=0.2) + + def test_shape_face_areas(self): + """Creates RotateSplineShapes and checks that the face areas are expected.""" + + assert len(self.test_shape.areas) == 1 + assert len(set(self.test_shape.areas)) == 1 + + self.test_shape.rotation_angle = 180 + assert len(self.test_shape.areas) == 3 + assert len(set([round(i) for i in self.test_shape.areas])) == 2 + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_parametric_shapes/test_RotateStraightShape.py b/tests/test_parametric_shapes/test_RotateStraightShape.py new file mode 100644 index 000000000..00627ddde --- /dev/null +++ b/tests/test_parametric_shapes/test_RotateStraightShape.py @@ -0,0 +1,227 @@ + +import math +import os +import unittest +from pathlib import Path + +import pytest +from paramak import RotateStraightShape + + +class TestRotateStraightShape(unittest.TestCase): + + def setUp(self): + self.test_shape = RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20), (20, 0)] + ) + + def test_largest_dimension(self): + """Checks that the largest_dimension is correct.""" + + assert self.test_shape.largest_dimension == 20 + + def test_default_parameters(self): + """Checks that the default parameters of a RotateStraightShape are correct.""" + + assert self.test_shape.rotation_angle == 360 + assert self.test_shape.stp_filename == "RotateStraightShape.stp" + assert self.test_shape.stl_filename == "RotateStraightShape.stl" + assert self.test_shape.azimuth_placement_angle == 0 + + def test_rotation_angle_getting_setting(self): + """Checks that the rotation_angle of a RotateStraightShape can be changed.""" + + assert self.test_shape.rotation_angle == 360 + self.test_shape.rotation_angle = 180 + assert self.test_shape.rotation_angle == 180 + + def test_absolute_shape_volume(self): + """Creates a RotateStraightShape and checks that its volume is correct.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume == pytest.approx(math.pi * (20**2) * 20) + + def test_relative_shape_volume(self): + """Creates two RotateStraightShapes and checks that their relative volumes + are correct.""" + + test_volume = self.test_shape.volume + self.test_shape.rotation_angle = 180 + assert test_volume == pytest.approx(self.test_shape.volume * 2) + + def test_union_volume_addition(self): + """Fuses two RotateStraightShapes and checks that their fused volume is + correct.""" + + inner_box = RotateStraightShape( + points=[(100, 100), (100, 200), (200, 200), (200, 100)], + ) + + outer_box = RotateStraightShape( + points=[(200, 100), (200, 200), (500, 200), (500, 100)], + ) + + outer_box_and_inner_box = RotateStraightShape( + points=[(200, 100), (200, 200), (500, 200), (500, 100)], + union=inner_box, + ) + + assert inner_box.volume + outer_box.volume == pytest.approx( + outer_box_and_inner_box.volume, rel=0.01 + ) + + def test_absolute_shape_areas(self): + """Creates RotateStraightShapes and checks that the areas of each face + are correct.""" + + assert self.test_shape.area == pytest.approx( + (math.pi * (20**2) * 2) + (math.pi * (20 * 2) * 20)) + assert len(self.test_shape.areas) == 3 + assert self.test_shape.areas.count( + pytest.approx(math.pi * (20**2))) == 2 + assert self.test_shape.areas.count( + pytest.approx(math.pi * (20 * 2) * 20)) == 1 + + self.test_shape.rotation_angle = 180 + assert self.test_shape.area == pytest.approx( + ((math.pi * (20**2) / 2) * 2) + (20 * 40) + (math.pi * (20 * 2) * 20 / 2)) + assert len(self.test_shape.areas) == 4 + assert self.test_shape.areas.count( + pytest.approx(math.pi * (20**2) / 2)) == 2 + assert self.test_shape.areas.count(pytest.approx(20 * 40)) == 1 + assert self.test_shape.areas.count( + pytest.approx((math.pi * (20 * 2) * 20) / 2)) == 1 + + test_shape = RotateStraightShape( + points=[(50, 0), (50, 50), (70, 50), (70, 0)], + ) + + assert test_shape.area == pytest.approx( + (((math.pi * (70**2)) - (math.pi * (50**2))) * 2) + (math.pi * (50 * 2) * 50) + (math.pi * (70 * 2) * 50)) + assert len(test_shape.areas) == 4 + assert test_shape.areas.count(pytest.approx( + (math.pi * (70**2)) - (math.pi * (50**2)))) == 2 + assert test_shape.areas.count( + pytest.approx(math.pi * (50 * 2) * 50)) == 1 + assert test_shape.areas.count( + pytest.approx(math.pi * (70 * 2) * 50)) == 1 + + test_shape.rotation_angle = 180 + assert test_shape.area == pytest.approx((20 * 50 * 2) + ((((math.pi * (70**2)) / 2) - ( + (math.pi * (50**2)) / 2)) * 2) + ((math.pi * (50 * 2) * 50) / 2) + ((math.pi * (70 * 2) * 50) / 2)) + assert len(test_shape.areas) == 6 + assert test_shape.areas.count(pytest.approx(20 * 50)) == 2 + assert test_shape.areas.count(pytest.approx( + ((math.pi * (70**2)) / 2) - ((math.pi * (50**2)) / 2))) == 2 + assert test_shape.areas.count( + pytest.approx(math.pi * (50 * 2) * 50 / 2)) == 1 + assert test_shape.areas.count( + pytest.approx(math.pi * (70 * 2) * 50 / 2)) == 1 + + def test_export_stp(self): + """Creates a RotateStraightShape and checks that a stp file of the + shape can be exported with the correct suffix using the export_stp + method.""" + + os.system("rm filename.stp filename.step") + self.test_shape.export_stp("filename.stp") + self.test_shape.export_stp("filename.step") + assert Path("filename.stp").exists() is True + assert Path("filename.step").exists() is True + os.system("rm filename.stp filename.step") + self.test_shape.export_stp("filename") + assert Path("filename.stp").exists() is True + os.system("rm filename.stp") + self.test_shape.export_stp() + assert Path("RotateStraightShape.stp").exists() is True + os.system("rm RotateStraightShape.stp") + + def test_export_stl(self): + """Creates a RotateStraightShape and checks that a stl file of the + shape can be exported with the correct suffix using the export_stl + method.""" + + os.system("rm filename.stl") + self.test_shape.export_stl("filename.stl") + assert Path("filename.stl").exists() is True + os.system("rm filename.stl") + self.test_shape.export_stl("filename") + assert Path("filename.stl").exists() is True + os.system("rm filename.stl") + + def test_export_svg(self): + """Creates a RotateStraightShape and checks that a svg file of the + shape can be exported with the correct suffix using the export_svg + method.""" + + os.system("rm filename.svg") + self.test_shape.export_svg("filename.svg") + assert Path("filename.svg").exists() is True + os.system("rm filename.svg") + self.test_shape.export_svg("filename") + assert Path("filename.svg").exists() is True + os.system("rm filename.svg") + + def test_cut_volume(self): + """Creates a RotateStraightShape with another RotateStraightShape + cut out and checks that the volume is correct.""" + + shape_with_cut = RotateStraightShape( + points=[(0, -5), (0, 25), (25, 25), (25, -5)], + cut=self.test_shape + ) + + assert shape_with_cut.volume == pytest.approx( + (math.pi * (25**2) * 30) - (math.pi * (20**2) * 20) + ) + + def test_multiple_cut_volume(self): + """Creates a RotateStraightShape with multiple RotateStraightShapes + cut out and checks that the volume is correct.""" + + main_shape = RotateStraightShape( + points=[(0, 0), (0, 200), (200, 200), (200, 0)], + ) + + shape_to_cut_1 = RotateStraightShape( + points=[(20, 0), (20, 200), (40, 200), (40, 0)], + ) + + shape_to_cut_2 = RotateStraightShape( + points=[(120, 0), (120, 200), (140, 200), (140, 0)], + ) + + main_shape_with_cuts = RotateStraightShape( + points=[(0, 0), (0, 200), (200, 200), (200, 0)], + cut=[shape_to_cut_1, shape_to_cut_2] + ) + + assert main_shape_with_cuts.volume == pytest.approx( + (math.pi * (200**2) * 200) - + ((math.pi * (40**2) * 200) - (math.pi * (20**2) * 200)) - + ((math.pi * (140**2) * 200) - (math.pi * (120**2) * 200)) + ) + + def test_hash_value(self): + """Creates a RotateStraightShape and checks that a cadquery solid with + a unique has value is created when .solid is called. Checks that the same + solid is returned when .solid is called again after no changes have been + made to the shape. Checks that a new solid with a new unique has is + constructed when .solid is called after changes to the shape have been + made. Checks that the hash_value of the shape is not updated until a new + solid has been constructed.""" + + assert self.test_shape.hash_value is None + assert self.test_shape.solid is not None + assert self.test_shape.hash_value is not None + initial_hash_value = self.test_shape.hash_value + assert self.test_shape.solid is not None + assert initial_hash_value == self.test_shape.hash_value + self.test_shape.rotation_angle = 180 + assert initial_hash_value == self.test_shape.hash_value + assert self.test_shape.solid is not None + assert initial_hash_value != self.test_shape.hash_value + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_parametric_shapes/test_SweepCircleShape.py b/tests/test_parametric_shapes/test_SweepCircleShape.py new file mode 100644 index 000000000..f97104142 --- /dev/null +++ b/tests/test_parametric_shapes/test_SweepCircleShape.py @@ -0,0 +1,135 @@ + +import math +import unittest + +import pytest +from paramak import RotateStraightShape, SweepCircleShape + + +class TestSweepCircleShape(unittest.TestCase): + + def setUp(self): + self.test_shape = SweepCircleShape( + radius=10, + path_points=[(50, 0), (30, 50), (70, 100), (50, 150)] + ) + + def test_default_parameters(self): + """Checks that the default parameters of a SweepCircleShape are correct.""" + + # assert self.test_shape.rotation_angle == 360 + assert self.test_shape.stp_filename == "SweepCircleShape.stp" + assert self.test_shape.stl_filename == "SweepCircleShape.stl" + assert self.test_shape.azimuth_placement_angle == 0 + assert self.test_shape.workplane == "XY" + assert self.test_shape.path_workplane == "XZ" + assert self.test_shape.force_cross_section == False + + def test_solid_construction_workplane(self): + """Checks that SweepSplineShapes can be created in different workplanes.""" + + self.test_shape.workplane = "YZ" + self.test_shape.path_workplane = "YX" + assert self.test_shape.solid is not None + + self.test_shape.workplane = "XZ" + self.test_shape.path_workplane = "XY" + assert self.test_shape.solid is not None + + def test_workplane_path_workplane_error_raises(self): + """Checks that errors are raised when SweepCircleShapes are created with + disallowed workplane and path_workplane combinations.""" + + def workplane_and_path_workplane_equal(): + self.test_shape.workplane = "XZ" + self.test_shape.path_workplane = "XZ" + + def invalid_relative_workplane_and_path_workplane(): + self.test_shape.workplane = "XZ" + self.test_shape.path_workplane = "YZ" + + self.assertRaises(ValueError, workplane_and_path_workplane_equal) + self.assertRaises( + ValueError, + invalid_relative_workplane_and_path_workplane) + + def test_absolute_shape_volume(self): + """Creates a SweepCircleShape and checks that the volume is correct.""" + + self.test_shape.path_points = [(50, 0), (50, 50), (50, 100)] + assert self.test_shape.solid is not None + assert self.test_shape.volume == pytest.approx(math.pi * 10**2 * 100) + + def test_relative_shape_volume_radius(self): + """Creates two SweepCircleShapes and checks that their relative volume + are correct.""" + + test_volume = self.test_shape.volume + self.test_shape.radius = 20 + assert self.test_shape.volume == pytest.approx( + test_volume * 4, rel=0.01) + + def test_relative_shape_volume_azimuthal_placement(self): + """Creates two SweepCircleShapes and checks that their relative volumes + are correct.""" + + test_volume = self.test_shape.volume + self.test_shape.azimuth_placement_angle = [0, 90, 180, 270] + assert self.test_shape.volume == pytest.approx( + test_volume * 4, rel=0.01) + + def test_points_equal_path_points(self): + """Checks that shape.points = shape.path_points upon construction of a + SweepCircleShape.""" + + assert self.test_shape.points == self.test_shape.path_points + + def test_html(self): + """Checks that a html graph of the path_points of a SweepCircleShape can be + exported using the export_html method.""" + + assert self.test_shape.export_html("filename.html") is not None + + def test_force_cross_section(self): + """Checks that a SweepCircleShape with the same cross-section at each path_point + is created when force_cross_section = True.""" + + self.test_shape.force_cross_section = True + + assert self.test_shape.areas.count( + pytest.approx(math.pi * (10**2), rel=0.01)) == 2 + + cutting_shape = RotateStraightShape( + points=[(0, 50), (0, 200), (100, 200), (100, 50)], + ) + self.test_shape.cut = cutting_shape + + assert self.test_shape.areas.count( + pytest.approx(math.pi * (10**2), rel=0.01)) == 2 + + cutting_shape = RotateStraightShape( + points=[(0, 100), (0, 200), (100, 200), (100, 100)] + ) + self.test_shape.cut = cutting_shape + + assert self.test_shape.areas.count( + pytest.approx(math.pi * (10**2), rel=0.01)) == 2 + + def test_force_cross_section_volume(self): + """Checks that a SweepCircleShape with a larger volume is created when + force_cross_section = True than when force_cross_section = False.""" + + test_volume = self.test_shape.volume + self.test_shape.force_cross_section = True + assert self.test_shape.volume > test_volume + + def test_surface_count(self): + """Creates a SweepCircleShape and checks that it has the correct number + of surfaces.""" + + assert len(self.test_shape.areas) == 3 + assert len(set(round(i) for i in self.test_shape.areas)) == 2 + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_parametric_shapes/test_SweepMixedShape.py b/tests/test_parametric_shapes/test_SweepMixedShape.py new file mode 100644 index 000000000..8285c154a --- /dev/null +++ b/tests/test_parametric_shapes/test_SweepMixedShape.py @@ -0,0 +1,128 @@ + +import unittest + +import pytest +from paramak import RotateStraightShape, SweepMixedShape + + +class TestSweepMixedShape(unittest.TestCase): + + def setUp(self): + self.test_shape = SweepMixedShape( + points=[(-10, -10, "straight"), (-10, 10, "spline"), (0, 20, "spline"), + (10, 10, "circle"), (0, 0, "circle"), (10, -10, "straight")], + path_points=[(50, 0), (30, 50), (70, 100), (50, 150)] + ) + + def test_default_parameters(self): + """Checks that the default parameters of a SweepMixedShape are correct.""" + + # assert self.test_shape.rotation_angle == 360 + assert self.test_shape.stp_filename == "SweepMixedShape.stp" + assert self.test_shape.stl_filename == "SweepMixedShape.stl" + assert self.test_shape.azimuth_placement_angle == 0 + assert self.test_shape.workplane == "XY" + assert self.test_shape.path_workplane == "XZ" + assert self.test_shape.force_cross_section == False + + def test_solid_construction_workplane(self): + """Checks that SweepMixedShapes can be created in different workplanes""" + + self.test_shape.workplane = "YZ" + self.test_shape.path_workplane = "YX" + assert self.test_shape.solid is not None + + self.test_shape.workplane = "XZ" + self.test_shape.path_workplane = "XY" + assert self.test_shape.solid is not None + + def test_relative_shape_volume_points(self): + """Creates two SweepMixedShapes and checks that their relative volumes + are correct.""" + + self.test_shape.points = [(-10, -10, "straight"), (-10, 10, "spline"), (0, 20, "spline"), + (10, 10, "circle"), (0, 0, "circle"), (10, -10, "straight")] + test_volume = self.test_shape.volume + self.test_shape.points = [(-20, -20, "straight"), (-20, 20, "spline"), (0, 40, "spline"), + (20, 20, "circle"), (0, 0, "circle"), (20, -20, "straight")] + assert self.test_shape.volume == pytest.approx( + test_volume * 4, rel=0.01) + + def test_relative_shape_volume_azimuthal_placement(self): + """Creates two SweepMixedShapes and checks that their relative volumes + are correct.""" + + test_volume = self.test_shape.volume + self.test_shape.azimuth_placement_angle = [0, 90, 180, 270] + assert self.test_shape.volume == pytest.approx( + test_volume * 4, rel=0.01) + + def test_workplane_path_workplane_error_raises(self): + """Checks that errors are raised when SweepMixedShapes are created with + disallowed workplane and path_workplane combinations.""" + + def workplane_and_path_workplane_equal(): + self.test_shape.workplane = "XZ" + self.test_shape.path_workplane = "XZ" + + def invalid_relative_workplane_and_path_workplane(): + self.test_shape.workplane = "XZ" + self.test_shape.path_workplane = "YZ" + + self.assertRaises(ValueError, workplane_and_path_workplane_equal) + self.assertRaises( + ValueError, + invalid_relative_workplane_and_path_workplane) + + def test_workplane_opposite_distance(self): + """Checks that a SweepMixedShape can be created with workplane XZ and + path_workplane XY.""" + + self.test_shape.workplane = "XZ" + self.test_shape.path_workplane = "XY" + + assert self.test_shape.solid is not None + + def test_force_cross_section(self): + """Checks that a SweepMixedshape with the same cross-section at each path_point + is created when force_cross_section = True.""" + + self.test_shape.force_cross_section = True + + test_area = round(min(self.test_shape.areas)) + + assert self.test_shape.areas.count( + pytest.approx(test_area, rel=0.01)) == 2 + + cutting_shape = RotateStraightShape( + points=[(0, 50), (0, 200), (100, 200), (100, 50)] + ) + self.test_shape.cut = cutting_shape + + assert self.test_shape.areas.count( + pytest.approx(test_area, rel=0.01)) == 2 + + cutting_shape.points = [(0, 100), (0, 200), (100, 200), (100, 100)] + self.test_shape.cut = cutting_shape + + assert self.test_shape.areas.count( + pytest.approx(test_area, rel=0.01)) == 2 + + def test_force_cross_section_volume(self): + """Checks that a SweepMixedShape with a larger volume is created when + force_cross_section = True than when force_cross_section = False.""" + + test_volume = self.test_shape.volume + self.test_shape.force_cross_section = True + assert self.test_shape.volume > test_volume + + def test_surface_count(self): + """Creates a SweepStraightShape and checks that it has the correct number + of surfaces.""" + + assert len(self.test_shape.areas) == 6 + assert len(set(round(i) for i in self.test_shape.areas)) == 5 + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_parametric_shapes/test_SweepSplineShape.py b/tests/test_parametric_shapes/test_SweepSplineShape.py new file mode 100644 index 000000000..4650456ae --- /dev/null +++ b/tests/test_parametric_shapes/test_SweepSplineShape.py @@ -0,0 +1,99 @@ + +import unittest + +import pytest +from paramak import RotateStraightShape, SweepSplineShape + + +class TestSweepSplineShape(unittest.TestCase): + + def setUp(self): + self.test_shape = SweepSplineShape( + points=[(-10, 10), (10, 10), (10, -10), (-10, -10)], + path_points=[(50, 0), (30, 50), (70, 100), (50, 150)] + ) + + def test_default_parameters(self): + """Checks that the default parameters of a SweepSplineShape are correct.""" + + # assert self.test_shape.rotation_angle == 360 + assert self.test_shape.stp_filename == "SweepSplineShape.stp" + assert self.test_shape.stl_filename == "SweepSplineShape.stl" + assert self.test_shape.azimuth_placement_angle == 0 + assert self.test_shape.workplane == "XY" + assert self.test_shape.path_workplane == "XZ" + assert self.test_shape.force_cross_section == False + + def test_solid_construction_workplane(self): + """Checks that SweepSplineShapes can be created in different workplanes.""" + + self.test_shape.workplane = "YZ" + self.test_shape.path_workplane = "YX" + assert self.test_shape.solid is not None + + self.test_shape.workplane = "XZ" + self.test_shape.path_workplane = "XY" + assert self.test_shape.solid is not None + + def test_relative_shape_volume_points(self): + """Creates two SweepSplineShapes and checks that their relative volumes + are correct.""" + + self.test_shape.points = [(-20, 20), (20, 20), (20, -20), (-20, -20)] + test_volume = self.test_shape.volume + self.test_shape.points = [(-10, 10), (10, 10), (10, -10), (-10, -10)] + assert self.test_shape.volume == pytest.approx( + test_volume * 0.25, rel=0.01) + + def test_relative_shape_volume_azimuthal_placement(self): + """Creates two SweepSplineShapes and checks that their relative volumes + are correct.""" + + test_volume = self.test_shape.volume + self.test_shape.azimuth_placement_angle = [0, 90, 180, 270] + assert self.test_shape.volume == pytest.approx( + test_volume * 4, rel=0.01) + + def test_force_cross_section(self): + """Checks that a SweepSplineShape with the same cross-section at each path_point + is created when force_cross_section = True.""" + + self.test_shape.force_cross_section = True + + test_area = round(min(self.test_shape.areas)) + + assert self.test_shape.areas.count( + pytest.approx(test_area, rel=0.01)) == 2 + + cutting_shape = RotateStraightShape( + points=[(0, 50), (0, 200), (100, 200), (100, 50)] + ) + self.test_shape.cut = cutting_shape + + assert self.test_shape.areas.count( + pytest.approx(test_area, rel=0.01)) == 2 + + cutting_shape.points = [(0, 100), (0, 200), (100, 200), (100, 100)] + self.test_shape.cut = cutting_shape + + assert self.test_shape.areas.count( + pytest.approx(test_area, rel=0.01)) == 2 + + def test_force_cross_section_volume(self): + """Checks that a SweepSplineShape with a larger volume is created when + force_cross_section = True than when force_cross_section = False.""" + + test_volume = self.test_shape.volume + self.test_shape.force_cross_section = True + assert self.test_shape.volume > test_volume + + def test_surface_count(self): + """Creates a SweepSplineShape and checks that it has the correct number + of surfaces.""" + + assert len(self.test_shape.areas) == 3 + assert len(set(round(i) for i in self.test_shape.areas)) == 2 + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_parametric_shapes/test_SweepStraightShape.py b/tests/test_parametric_shapes/test_SweepStraightShape.py new file mode 100644 index 000000000..668567b8c --- /dev/null +++ b/tests/test_parametric_shapes/test_SweepStraightShape.py @@ -0,0 +1,87 @@ + +import unittest + +import pytest +from paramak import RotateStraightShape, SweepStraightShape + + +class TestSweepStraightShape(unittest.TestCase): + + def setUp(self): + self.test_shape = SweepStraightShape( + points=[(-10, 10), (10, 10), (10, -10), (-10, -10)], + path_points=[(50, 0), (30, 50), (70, 100), (50, 150)] + ) + + def test_default_parameters(self): + """Checks that the default parameters of a SweepStraightShape are correct.""" + + # assert self.test_shape.rotation_angle == 360 + assert self.test_shape.stp_filename == "SweepStraightShape.stp" + assert self.test_shape.stl_filename == "SweepStraightShape.stl" + assert self.test_shape.azimuth_placement_angle == 0 + assert self.test_shape.workplane == "XY" + assert self.test_shape.path_workplane == "XZ" + assert self.test_shape.force_cross_section == False + + def test_absolute_shape_volume(self): + """Creates a SweepStraightShape and checks that the volume is correct.""" + + self.test_shape.path_points = [(50, 0), (50, 50), (50, 100)] + assert self.test_shape.solid is not None + assert self.test_shape.volume == pytest.approx(20 * 20 * 100) + + def test_relative_shape_volume(self): + """Creates two SweepStraightShapes and checks that their relative volumes + are correct.""" + + test_volume = self.test_shape.volume + self.test_shape.points = [(-20, 20), (20, 20), (20, -20), (-20, -20)] + assert self.test_shape.volume == pytest.approx(test_volume * 4) + + def test_relative_shape_volume(self): + """Creates two SweepStraightShapes and checks that their relative volumes + are correct.""" + + test_volume = self.test_shape.volume + self.test_shape.azimuth_placement_angle = [0, 90, 180, 270] + assert self.test_shape.volume == pytest.approx(test_volume * 4) + + def test_force_cross_section(self): + """Checks that a SweepStraightShape with the same cross-section at each path_point + is created when force_cross_section = True.""" + + self.test_shape.force_cross_section = True + + assert self.test_shape.areas.count(pytest.approx(400, rel=0.01)) == 2 + + cutting_shape = RotateStraightShape( + points=[(0, 50), (0, 200), (100, 200), (100, 50)], + ) + self.test_shape.cut = cutting_shape + + assert self.test_shape.areas.count(pytest.approx(400, rel=0.01)) == 2 + + cutting_shape.points = [(0, 100), (0, 200), (100, 200), (100, 100)] + self.test_shape.cut = cutting_shape + + assert self.test_shape.areas.count(pytest.approx(400, rel=0.01)) == 2 + + def test_force_cross_section_volume(self): + """Checks that a SweepStraightShape with a larger volume is created when + force_cross_section = True than when force_cross_section = False.""" + + test_volume = self.test_shape.volume + self.test_shape.force_cross_section = True + assert self.test_shape.volume > test_volume + + def test_surface_count(self): + """Creates a SweepStraightShape and checks that it has the correct number + of surfaces.""" + + assert len(self.test_shape.areas) == 6 + assert len(set(round(i) for i in self.test_shape.areas)) == 3 + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 000000000..bf1564f85 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,60 @@ + +import unittest + +import numpy as np +import paramak +from paramak.utils import (EdgeLengthSelector, FaceAreaSelector, + find_center_point_of_circle) + + +class TestUtilityFunctions(unittest.TestCase): + + def test_find_center_point_of_circle(self): + """passes three points on a circle to the function and checks that the + radius and center of the circle is calculated correctly""" + + point_1 = (0, 20) + point_2 = (20, 0) + point_3 = (0, -20) + + assert find_center_point_of_circle( + point_1, point_2, point_3) == ( + (0, 0), 20) + + def test_EdgeLengthSelector_with_fillet_areas(self): + """tests the filleting of a RotateStraightShape results in an extra + surface area""" + + test_shape = paramak.RotateStraightShape( + points=[(1, 1), (2, 1), (2, 2)]) + + assert len(test_shape.areas) == 3 + + test_shape.solid = test_shape.solid.edges( + EdgeLengthSelector(6.28)).fillet(0.1) + + assert len(test_shape.areas) == 4 + + def test_FaceAreaSelector_with_fillet_areas(self): + """tests the filleting of a ExtrudeStraightShape""" + + test_shape = paramak.ExtrudeStraightShape( + distance=5, points=[(1, 1), (2, 1), (2, 2)]) + + assert len(test_shape.areas) == 5 + + test_shape.solid = test_shape.solid.faces( + FaceAreaSelector(0.5)).fillet(0.1) + + assert len(test_shape.areas) == 11 + + def test_find_center_point_of_circle_zero_det(self): + """Checks that None is given if det is zero + """ + point_1 = (0, 0) + point_2 = (0, 0) + point_3 = (0, 0) + + assert find_center_point_of_circle( + point_1, point_2, point_3) == ( + None, np.inf)