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 @@
+
+[](https://www.python.org)
+[](https://circleci.com/gh/ukaea/paramak/tree/main)
+[](https://codecov.io/gh/ukaea/paramak)
+[](https://badge.fury.io/py/paramak)
+[](https://paramak.readthedocs.io/en/main/?badge=main)
+[](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
+
+
`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)