diff --git a/.circleci/config.yml b/.circleci/config.yml index f2e841b52..2277539dc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -20,6 +20,11 @@ jobs: command: pytest tests/test_neutronics_utils.py -v --cov=paramak --cov-report term --cov-report xml --junitxml=test-reports/junit.xml + - run: + name: run neutronics_example tests + command: + pytest tests/test_example_neutronics_simulations.py -v --cov=paramak --cov-report term --cov-report xml --junitxml=test-reports/junit.xml + - run: name: run utils tests command: diff --git a/.github/workflows/docker_ci.yml b/.github/workflows/docker_ci.yml index 566f6c368..e1c414a14 100644 --- a/.github/workflows/docker_ci.yml +++ b/.github/workflows/docker_ci.yml @@ -13,5 +13,5 @@ jobs: - uses: actions/checkout@v1 - name: Build and test with Docker run: | - docker build -t paramak --build-arg include_neutronics=true --build-arg cq_version=master --build-arg compile_cores=2 . - docker run --rm paramak /bin/bash -c "cd .. && bash run_tests.sh && curl -s https://codecov.io/bash | bash" + docker build -t paramak --build-arg include_neutronics=true --build-arg cq_version=2.1 --build-arg compile_cores=2 . + docker run --rm paramak /bin/bash -c "bash run_tests.sh && curl -s https://codecov.io/bash | bash" diff --git a/.gitignore b/.gitignore index 0ab77b6b6..c43101aef 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,7 @@ coverage.xml *.vtk *.jpg *.xcf +*.gif # sphinx built documetation docs/build/ diff --git a/Dockerfile b/Dockerfile index 0b973a13d..a5440de05 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,7 +40,7 @@ # docker run --rm ukaea/paramak pytest /tests # docker run --rm ukaea/paramak /bin/bash -c "cd .. && bash run_tests.sh" -FROM continuumio/miniconda3 +FROM continuumio/miniconda3:4.9.2 # By default this Dockerfile builds with the latest release of CadQuery 2 ARG cq_version=2 @@ -164,9 +164,11 @@ RUN if [ "$include_neutronics" = "true" ] ; \ cd build ; \ cmake ../DAGMC -DBUILD_TALLY=ON \ -DMOAB_DIR=/MOAB \ + -DDOUBLE_DOWN=ON \ -DBUILD_STATIC_EXE=OFF \ -DBUILD_STATIC_LIBS=OFF \ - -DCMAKE_INSTALL_PREFIX=/DAGMC/ ; \ + -DCMAKE_INSTALL_PREFIX=/DAGMC/ \ + -DDOUBLE_DOWN_DIR=/double-down ; \ make -j"$compile_cores" install ; \ rm -rf /DAGMC/DAGMC /DAGMC/build ; \ fi @@ -211,5 +213,3 @@ 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/DockerfileDependencies b/DockerfileDependencies index 9b1462145..4d8111f76 100644 --- a/DockerfileDependencies +++ b/DockerfileDependencies @@ -40,7 +40,7 @@ # docker run --rm ukaea/paramak pytest /tests # docker run --rm ukaea/paramak /bin/bash -c "cd .. && bash run_tests.sh" -FROM continuumio/miniconda3 +FROM continuumio/miniconda3:4.9.2 # By default this Dockerfile builds with the latest release of CadQuery 2 ARG cq_version=2 @@ -164,9 +164,11 @@ RUN if [ "$include_neutronics" = "true" ] ; \ cd build ; \ cmake ../DAGMC -DBUILD_TALLY=ON \ -DMOAB_DIR=/MOAB \ + -DDOUBLE_DOWN=ON \ -DBUILD_STATIC_EXE=OFF \ -DBUILD_STATIC_LIBS=OFF \ - -DCMAKE_INSTALL_PREFIX=/DAGMC/ ; \ + -DCMAKE_INSTALL_PREFIX=/DAGMC/ \ + -DDOUBLE_DOWN_DIR=/double-down ; \ make -j"$compile_cores" install ; \ rm -rf /DAGMC/DAGMC /DAGMC/build ; \ fi diff --git a/README.md b/README.md index 936496102..1ad5097b4 100644 --- a/README.md +++ b/README.md @@ -153,8 +153,8 @@ common reactor components.

- - + +

## Selection Of Parametric Components diff --git a/docs/source/paramak.parametric_components.rst b/docs/source/paramak.parametric_components.rst index 1f5bc7c8c..d4684ded5 100644 --- a/docs/source/paramak.parametric_components.rst +++ b/docs/source/paramak.parametric_components.rst @@ -217,6 +217,20 @@ CuttingWedgeFS() :members: :show-inheritance: +HexagonPin() +^^^^^^^^^^^^ + +|HexagonPinstp| |HexagonPinsvg| + +.. |HexagonPinstp| image:: https://user-images.githubusercontent.com/8583900/107092190-07307300-67fb-11eb-995c-b5622de717ee.png + :width: 300px +.. |HexagonPinsvg| image:: https://user-images.githubusercontent.com/8583900/107092487-9c336c00-67fb-11eb-8eb1-755462493140.png + :width: 300px + +.. automodule:: paramak.parametric_components.hexagon_pin + :members: + :show-inheritance: + InboardFirstwallFCCS() ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/examples/example_neutronics_simulations/shape_with_gas_production.py b/examples/example_neutronics_simulations/shape_with_gas_production.py new file mode 100644 index 000000000..7e9e8d1cf --- /dev/null +++ b/examples/example_neutronics_simulations/shape_with_gas_production.py @@ -0,0 +1,49 @@ +"""Demonstrates the use of reaction rates in the cell tally. +(n,Xp) is MT number 203 and scores all proton (hydrogen) production +(n,Xt) is MT number 205 and scores all tritium production +(n,Xa) is MT number 207 and scores all alpha paticle (helium) production +https://docs.openmc.org/en/latest/usersguide/tallies.html#scores +""" + + +import openmc +import paramak + + +def main(): + my_shape = paramak.CenterColumnShieldHyperbola( + height=500, + inner_radius=50, + mid_radius=60, + outer_radius=100, + material_tag='center_column_shield_mat' + ) + + # makes the openmc neutron source at x,y,z 0, 0, 0 with isotropic + # directions + source = openmc.Source() + source.space = openmc.stats.Point((0, 0, 0)) + source.energy = openmc.stats.Discrete([14e6], [1]) + source.angle = openmc.stats.Isotropic() + + # converts the geometry into a neutronics geometry + my_model = paramak.NeutronicsModel( + geometry=my_shape, + source=source, + materials={'center_column_shield_mat': 'Be'}, + cell_tallies=['(n,Xa)', '(n,Xt)', '(n,Xp)'], + mesh_tally_3d=['(n,Xa)', '(n,Xt)', '(n,Xp)'], + mesh_tally_2d=['(n,Xa)', '(n,Xt)', '(n,Xp)'], + simulation_batches=10, + simulation_particles_per_batch=200 + ) + + # performs an openmc simulation on the model + my_model.simulate(method='pymoab') + + # this extracts the values from the results dictionary + print(my_model.results) + + +if __name__ == "__main__": + main() diff --git a/examples/example_parametric_components/make_all_parametric_components.py b/examples/example_parametric_components/make_all_parametric_components.py index da16d3634..be6ec30bb 100644 --- a/examples/example_parametric_components/make_all_parametric_components.py +++ b/examples/example_parametric_components/make_all_parametric_components.py @@ -306,7 +306,7 @@ def main(): ) all_components.append(component) - component = paramak.ToroidalFieldCoilPrincetonD( + magnet = paramak.ToroidalFieldCoilPrincetonD( R1=80, R2=300, thickness=30, @@ -314,7 +314,7 @@ def main(): number_of_coils=1, stp_filename="toroidal_field_coil_princeton_d.stp" ) - all_components.append(component) + all_components.append(magnet) component = paramak.ITERtypeDivertor( # default parameters @@ -402,6 +402,28 @@ def main(): ) all_components.append(component) + component = paramak.PoloidalSegments( + number_of_segments=5, + center_point=(400, 50) + ) + all_components.append(component) + + component = paramak.TFCoilCasing( + magnet=magnet, + inner_offset=10, + outer_offset=15, + vertical_section_offset=20, + distance=40 + ) + all_components.append(component) + + component = paramak.HexagonPin( + length_of_side=5, + distance=10, + center_point=(10, 20) + ) + all_components.append(component) + return all_components @@ -410,5 +432,6 @@ def main(): filenames = [] for components in all_components: components.export_stp() + components.export_html(components.stp_filename[:-4] + '.html') filenames.append(components.stp_filename) print(filenames) diff --git a/examples/example_parametric_reactors/htc_reactor.py b/examples/example_parametric_reactors/htc_reactor.py index 4ada138e0..c93d4eff0 100644 --- a/examples/example_parametric_reactors/htc_reactor.py +++ b/examples/example_parametric_reactors/htc_reactor.py @@ -199,7 +199,7 @@ def main(rotation_angle=180): stp_filename='vacvessel.stp' ) - inner_vessel = paramak.RotateStraightShape( + inner_vessel = paramak.RotateMixedShape( points=[ (269.7459584295612, -46.54377880184336, 'straight'), (231.87066974595842, -46.5437788018433, 'spline'), @@ -258,6 +258,7 @@ def main(rotation_angle=180): sparc.export_stp() sparc.export_svg('htc_reactor.svg') + sparc.export_html('htc_reactor.html') if __name__ == "__main__": diff --git a/examples/example_parametric_reactors/make_all_reactors.py b/examples/example_parametric_reactors/make_all_reactors.py index ed06e6ee6..346c2090e 100644 --- a/examples/example_parametric_reactors/make_all_reactors.py +++ b/examples/example_parametric_reactors/make_all_reactors.py @@ -10,7 +10,7 @@ from submersion_reactor_single_null import make_submersion_sn -def main(outputs=['stp', 'svg']): +def main(outputs=['stp', 'svg', 'html']): make_submersion(outputs=outputs) make_submersion_sn(outputs=outputs) diff --git a/examples/example_parametric_reactors/make_animation.py b/examples/example_parametric_reactors/make_animation.py index 4a0b8c041..562388ca3 100644 --- a/examples/example_parametric_reactors/make_animation.py +++ b/examples/example_parametric_reactors/make_animation.py @@ -1,57 +1,114 @@ -__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 """ +__doc__ = """ Creates a series of images of a ball reactor images and combines +them into gif animations using the command line tool convert, you will need to +have imagemagick installed to convert the svg images to a gif animation """ -import argparse -import os -import uuid +import subprocess import numpy as np import paramak -from tqdm import tqdm +from scipy.interpolate import interp1d -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)): +def rotate_single_reactor(number_of_images=100): + """Makes a single reactor and exports and svg image with different view + angles. Combines the svg images into a gif animation.""" - 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, + # allows the projection angle for the svg to be found via interpolation + angle_finder = interp1d([0, number_of_images], [2.4021, 6.]) + + 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=np.random.uniform(20, 200), + plasma_radial_thickness=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), + firstwall_radial_thickness=30, + blanket_rear_wall_radial_thickness=30, 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, + 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=100, - outboard_tf_coil_poloidal_thickness=50, + outboard_tf_coil_radial_thickness=30, + outboard_tf_coil_poloidal_thickness=30, + tf_coil_to_rear_blanket_radial_gap=20, ) - 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" - ) + for i in range(number_of_images): + + # uses the rotation angle (in radians) to find new x, y points + x_vec, y_vec = paramak.utils.rotate([0, 0], [1, 0], angle_finder(i)) + projectionDir = (x_vec, y_vec, 0) + + my_reactor.export_svg( + filename="rotation_" + str(i).zfill(4) + ".svg", + projectionDir=projectionDir, + showHidden=False, + height=200, + width=300, + marginTop=27, + marginLeft=35, + strokeWidth=3.5 + ) + + print("made", str(i + 1), "models out of", str(number_of_images)) + + subprocess.check_call( + ["convert", "-delay", "15", "rotation_*.svg", "rotated.gif"]) + + print("animation file made as saved as rotated.gif") + + +def make_random_reactors(number_of_images=11): + """Makes a series of random sized reactors and exports an svg image for + each one. Combines the svg images into a gif animation.""" + + # makes a series of reactor models + for i in range(number_of_images): + + 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.3, 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=[30, 30, 30, 30], + pf_coil_to_rear_blanket_radial_gap=20, + pf_coil_to_tf_coil_radial_gap=50, + outboard_tf_coil_radial_thickness=100, + outboard_tf_coil_poloidal_thickness=50, + ) + + my_reactor.export_svg( + filename="random_" + str(i).zfill(4) + ".svg", + showHidden=False + ) - print(str(args.number_of_models), "models made") + print("made", str(i + 1), "models out of", str(number_of_images)) -os.system("convert -delay 40 output_for_animation_2d/*.png 2d.gif") + subprocess.check_call( + ["convert", "-delay", "40", "random_*.svg", "randoms.gif"]) -os.system("convert -delay 40 output_for_animation_3d/*.png 3d.gif") + print("animation file made as saved as randoms.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") +if __name__ == "__main__": + rotate_single_reactor() + # make_random_reactors() diff --git a/examples/example_parametric_reactors/segmented_blanket_ball_reactor.py b/examples/example_parametric_reactors/segmented_blanket_ball_reactor.py index 146dcde77..e0c3f5e10 100644 --- a/examples/example_parametric_reactors/segmented_blanket_ball_reactor.py +++ b/examples/example_parametric_reactors/segmented_blanket_ball_reactor.py @@ -37,6 +37,8 @@ def make_ball_reactor_seg(outputs=['stp', 'neutronics', 'svg', 'stl', 'html']): blanket_fillet_radius=15, ) + my_reactor.solid # triggers the construction of the reactor model + # finds the correct edges to fillet x_coord = my_reactor.major_radius front_face = my_reactor._blanket.solid.faces( @@ -51,8 +53,6 @@ def make_ball_reactor_seg(outputs=['stp', 'neutronics', 'svg', 'stl', 'html']): 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: diff --git a/examples/example_parametric_shapes/make_CAD_from_points.py b/examples/example_parametric_shapes/make_CAD_from_points.py index 00d215dc5..5c394c6d6 100644 --- a/examples/example_parametric_shapes/make_CAD_from_points.py +++ b/examples/example_parametric_shapes/make_CAD_from_points.py @@ -16,6 +16,7 @@ def main(): points=[(400, 100), (400, 200), (600, 200), (600, 100)] ) rotated_straights.export_stp("rotated_straights.stp") + rotated_straights.export_svg("rotated_straights.svg") rotated_straights.export_html("rotated_straights.html") # this makes a banana shape and rotates it to make a solid @@ -33,6 +34,7 @@ def main(): ] ) rotated_spline.export_stp("rotated_spline.stp") + rotated_spline.export_svg("rotated_spline.svg") rotated_spline.export_html("rotated_spline.html") # this makes a shape with straight, spline and circular edges and rotates @@ -50,6 +52,7 @@ def main(): ] ) rotated_mixed.export_stp("rotated_mixed.stp") + rotated_mixed.export_svg("rotated_mixed.svg") rotated_mixed.export_html("rotated_mixed.html") # this makes a circular shape and rotates it to make a solid @@ -60,6 +63,7 @@ def main(): workplane="XZ" ) rotated_circle.export_stp("rotated_circle.stp") + rotated_circle.export_svg("rotated_circle.svg") rotated_circle.export_html("rotated_circle.html") # extrude examples @@ -78,6 +82,7 @@ def main(): ] ) extruded_straight.export_stp("extruded_straight.stp") + extruded_straight.export_svg("extruded_straight.svg") extruded_straight.export_html("extruded_straight.html") # this makes a banana shape and rotates it to make a solid @@ -95,6 +100,7 @@ def main(): ] ) extruded_spline.export_stp("extruded_spline.stp") + extruded_spline.export_svg("extruded_spline.svg") extruded_spline.export_html("extruded_spline.html") # this makes a banana shape straight top and bottom edges and extrudes it @@ -112,6 +118,7 @@ def main(): ], ) extruded_mixed.export_stp("extruded_mixed.stp") + extruded_mixed.export_svg("extruded_mixed.svg") extruded_mixed.export_html("extruded_mixed.html") # this makes a circular shape and extrudes it to make a solid @@ -121,6 +128,7 @@ def main(): distance=200 ) extruded_circle.export_stp("extruded_circle.stp") + extruded_circle.export_svg("extruded_circle.svg") extruded_circle.export_html("extruded_circle.html") # sweep examples @@ -147,6 +155,7 @@ def main(): path_workplane="XZ" ) sweep_straight.export_stp("sweep_straight.stp") + sweep_straight.export_svg("sweep_straight.svg") sweep_straight.export_html("sweep_straight.html") # this makes a banana shape with spline edges and sweeps it along a spline @@ -173,6 +182,7 @@ def main(): path_workplane="XZ" ) sweep_spline.export_stp("sweep_spline.stp") + sweep_spline.export_svg("sweep_spline.svg") sweep_spline.export_html("sweep_spline.html") # this makes a shape with straight, spline and circular edges and sweeps @@ -198,6 +208,7 @@ def main(): path_workplane="XZ" ) sweep_mixed.export_stp("sweep_mixed.stp") + sweep_mixed.export_svg("sweep_mixed.svg") sweep_mixed.export_html("sweep_mixed.html") # this makes a circular shape and sweeps it to make a solid @@ -214,6 +225,7 @@ def main(): path_workplane="XZ" ) sweep_circle.export_stp("sweep_circle.stp") + sweep_circle.export_svg("sweep_circle.svg") sweep_circle.export_html("sweep_circle.html") diff --git a/examples/example_parametric_shapes/make_html_diagram_from_stp_file.py b/examples/example_parametric_shapes/make_html_diagram_from_stp_file.py new file mode 100644 index 000000000..e2efffdb0 --- /dev/null +++ b/examples/example_parametric_shapes/make_html_diagram_from_stp_file.py @@ -0,0 +1,75 @@ +"""Creates a stp file and then loads up the stp file and then facets the wires +(edges) of the geometry and plots the faceted eges along with the vertices +within the stp file.""" + +import paramak + + +def make_stp_file(): + """Creates an example stp file for plotting html point graphs""" + + # this creates a Shape object + example_shape = paramak.ExtrudeMixedShape( + distance=1, + points=[ + (100, 0, "straight"), + (200, 0, "circle"), + (250, 50, "circle"), + (200, 100, "straight"), + (150, 100, "spline"), + (140, 75, "spline"), + (110, 45, "spline"), + ] + ) + + # this exports the shape as a html image with a few different view planes + example_shape.export_html("example_shape_RZ.html") + example_shape.export_html("example_shape_XYZ.html", view_plane='XYZ') + example_shape.export_html("example_shape_XZ.html", view_plane='XZ') + + # This exports the Shape object as an stp file that will be imported later + example_shape.export_stp("example_shape.stp") + + +def load_stp_file_and_plot(): + """Loads an stp file and plots html point graphs""" + + # loads the stp file and obtains the solid shape and list of wires / edges + solid, wires = paramak.utils.load_stp_file( + filename="example_shape.stp", + ) + + # produces a plot on the R (radius) Z axis and saves the html file + paramak.utils.export_wire_to_html( + wires=wires, + tolerance=0.1, + view_plane="RZ", + facet_splines=True, + facet_circles=True, + filename="example_shape_from_stp_RZ.html", + ) + + # produces a plot on the XZ axis and saves the html file + paramak.utils.export_wire_to_html( + wires=wires, + tolerance=0.1, + view_plane="XZ", + facet_splines=True, + facet_circles=True, + filename="example_shape_from_stp_XZ.html", + ) + + # produces a 3D plot with XYZ axis and saves the html file + paramak.utils.export_wire_to_html( + wires=wires, + tolerance=0.1, + view_plane="XYZ", + facet_splines=True, + facet_circles=True, + filename="example_shape_from_stp_XYZ.html", + ) + + +if __name__ == "__main__": + make_stp_file() + load_stp_file_and_plot() diff --git a/paramak/__init__.py b/paramak/__init__.py index 52c7e4490..c3c002583 100644 --- a/paramak/__init__.py +++ b/paramak/__init__.py @@ -20,6 +20,8 @@ from .parametric_shapes.sweep_straight_shape import SweepStraightShape from .parametric_shapes.sweep_circle_shape import SweepCircleShape +from .parametric_components.hexagon_pin import HexagonPin + from .parametric_components.tokamak_plasma import Plasma from .parametric_components.tokamak_plasma_from_points import PlasmaFromPoints from .parametric_components.tokamak_plasma_plasmaboundaries import PlasmaBoundaries diff --git a/paramak/neutronics_utils.py b/paramak/neutronics_utils.py index 01cd12885..00e7aac34 100644 --- a/paramak/neutronics_utils.py +++ b/paramak/neutronics_utils.py @@ -112,7 +112,7 @@ def add_stl_to_moab_core( 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, @@ -195,7 +195,7 @@ def get_neutronics_results_from_statepoint_file( 'std. dev.': tally_std_dev, } - if tally.name.endswith('heating'): + elif tally.name.endswith('heating'): data_frame = tally.get_pandas_dataframe() tally_result = data_frame["mean"].sum() @@ -211,7 +211,7 @@ def get_neutronics_results_from_statepoint_file( 'std. dev.': tally_std_dev * 1.602176487e-19 * (fusion_power / ((17.58 * 1e6) / 6.2415090744e18)), } - if tally.name.endswith('flux'): + elif tally.name.endswith('flux'): data_frame = tally.get_pandas_dataframe() tally_result = data_frame["mean"].sum() @@ -221,7 +221,7 @@ def get_neutronics_results_from_statepoint_file( 'std. dev.': tally_std_dev, } - if tally.name.endswith('spectra'): + elif tally.name.endswith('spectra'): data_frame = tally.get_pandas_dataframe() tally_result = data_frame["mean"] tally_std_dev = data_frame['std. dev.'] @@ -231,31 +231,20 @@ def get_neutronics_results_from_statepoint_file( 'std. dev.': tally_std_dev.tolist(), } - if tally.name.startswith('tritium_production_on_2D_mesh'): - + elif '_on_2D_mesh' in tally.name: + score = tally.name.split('_')[0] _save_2d_mesh_tally_as_png( - score='(n,Xt)', + score=score, tally=tally, - filename='tritium_production_on_2D_mesh' + tally.name[-3:] - ) - - if tally.name.startswith('flux_on_2D_mesh'): - - _save_2d_mesh_tally_as_png( - score='flux', - tally=tally, - filename='flux_on_2D_mesh' + tally.name[-3:] - ) - - if tally.name.startswith('heating_on_2D_mesh'): - - _save_2d_mesh_tally_as_png( - score='heating', - tally=tally, - filename='heating_on_2D_mesh' + tally.name[-3:] - ) - - if '_on_3D_mesh' in tally.name: + filename=tally.name.replace( + '(', + '').replace( + ')', + '').replace( + ',', + '-')) + + elif '_on_3D_mesh' in tally.name: mesh_id = 1 mesh = statepoint.meshes[mesh_id] @@ -294,8 +283,24 @@ def get_neutronics_results_from_statepoint_file( tally_label=tally.name, tally_data=data, error_data=error, - outfile=tally.name + '.vtk' - ) + outfile=tally.name.replace( + '(', + '').replace( + ')', + '').replace( + ',', + '-') + + '.vtk') + + else: + # this must be a standard score cell tally + data_frame = tally.get_pandas_dataframe() + tally_result = data_frame["mean"].sum() + tally_std_dev = data_frame['std. dev.'].sum() + results[tally.name]['events per source particle'] = { + 'result': tally_result, + 'std. dev.': tally_std_dev, + } return results diff --git a/paramak/parametric_components/blanket_fp.py b/paramak/parametric_components/blanket_fp.py index 153ddf755..152739b1b 100644 --- a/paramak/parametric_components/blanket_fp.py +++ b/paramak/parametric_components/blanket_fp.py @@ -281,23 +281,23 @@ def create_physical_groups(self): """ groups = [] - nb_volumes = 1 # only one volume - nb_surfaces = 2 # inner and outer + number_of_volumes = 1 # only one volume + number_of_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 + number_of_surfaces += 2 surface_names += ["left_section", "right_section"] - full_rot = False + full_rotation = False else: - full_rot = True + full_rotation = 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 + number_of_surfaces += 2 surface_names += ["inner_section", "outer_section"] stop_equals_start = False else: @@ -305,7 +305,7 @@ def create_physical_groups(self): # rearrange order new_order = [i for i in range(len(surface_names))] - if full_rot: + if full_rotation: if not stop_equals_start: # from ["inner", "outer", "inner_section", "outer_section"] @@ -327,10 +327,10 @@ def create_physical_groups(self): 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): + for i in range(1, number_of_volumes + 1): group = {"dim": 3, "id": i, "name": volumes_names[i - 1]} groups.append(group) - for i in range(1, nb_surfaces + 1): + for i in range(1, number_of_surfaces + 1): group = {"dim": 2, "id": i, "name": surface_names[i - 1]} groups.append(group) self.physical_groups = groups diff --git a/paramak/parametric_components/blanket_poloidal_segment.py b/paramak/parametric_components/blanket_poloidal_segment.py index 6034cbe1d..af9b05549 100644 --- a/paramak/parametric_components/blanket_poloidal_segment.py +++ b/paramak/parametric_components/blanket_poloidal_segment.py @@ -58,11 +58,11 @@ def segments_angles(self, value): 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) + warnings.warn(msg) elif self.num_segments is not None: msg = "num_segment attribute will be ignored if " + \ "segments_angles is not None" - warnings.warn(msg, UserWarning) + warnings.warn(msg) self._segments_angles = value @property diff --git a/paramak/parametric_components/cutting_wedge_fs.py b/paramak/parametric_components/cutting_wedge_fs.py index 2749b26a0..29fe0b345 100644 --- a/paramak/parametric_components/cutting_wedge_fs.py +++ b/paramak/parametric_components/cutting_wedge_fs.py @@ -1,5 +1,5 @@ -from collections import Iterable +from collections.abc import Iterable from operator import itemgetter from paramak import CuttingWedge diff --git a/paramak/parametric_components/hexagon_pin.py b/paramak/parametric_components/hexagon_pin.py new file mode 100644 index 000000000..a4eea6f3c --- /dev/null +++ b/paramak/parametric_components/hexagon_pin.py @@ -0,0 +1,84 @@ + +import math +from typing import Optional, Tuple + +from paramak import ExtrudeStraightShape + + +class HexagonPin(ExtrudeStraightShape): + """Creates an extruded hexagon by a provided distance about a center point. + + Args: + length_of_side: the length of one side of the hexagon (mm). + distance: extruded distance along the y-direction (mm). + center_point: the center of the hexagon on the x-z plane (mm). + stp_filename: defaults to "HexagonPin.stp". + stl_filename: defaults to "HexagonPin.stl". + name: defaults to "hexagon_pin". + material_tag: defaults to "hexagon_pin_mat". + """ + + def __init__( + self, + length_of_side: float, + distance: float, + center_point: Tuple[float, float] = (0, 0), + stp_filename: Optional[str] = "HexagonPin.stp", + stl_filename: Optional[str] = "HexagonPin.stl", + name: Optional[str] = "hexagon_pin", + material_tag: Optional[str] = "hexagon_pin_mat", + **kwargs + ) -> None: + + super().__init__( + name=name, + distance=distance, + material_tag=material_tag, + stp_filename=stp_filename, + stl_filename=stl_filename, + **kwargs + ) + + self.center_point = center_point + self.length_of_side = length_of_side + + @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 length_of_side(self): + return self._length_of_side + + @length_of_side.setter + def length_of_side(self, length_of_side): + self._length_of_side = length_of_side + + def find_points(self): + """Finds the XZ points joined by straight connections that describe + the 2D profile of the hexagon faced shape.""" + + # map of points + # p3 ---- p2 + # - - + # - - + # - - + # p4 p1 + # - - + # - - + # - - + # p5 ---- p6 + + points = [] + for i in range(6): + point = (self.length_of_side * math.cos(math.pi / 3 * i) + + self.center_point[0], + self.length_of_side * math.sin(math.pi / 3 * i) + + self.center_point[1]) + points.append(point) + + self.points = points diff --git a/paramak/parametric_components/inboard_firstwall_fccs.py b/paramak/parametric_components/inboard_firstwall_fccs.py index c80cf1834..ffb65760f 100644 --- a/paramak/parametric_components/inboard_firstwall_fccs.py +++ b/paramak/parametric_components/inboard_firstwall_fccs.py @@ -1,5 +1,5 @@ -from collections import Iterable +from collections.abc import Iterable from paramak import (CenterColumnShieldCircular, CenterColumnShieldCylinder, CenterColumnShieldFlatTopCircular, diff --git a/paramak/parametric_components/poloidal_field_coil_case_set.py b/paramak/parametric_components/poloidal_field_coil_case_set.py index 0d52202a5..e90b29fbb 100644 --- a/paramak/parametric_components/poloidal_field_coil_case_set.py +++ b/paramak/parametric_components/poloidal_field_coil_case_set.py @@ -169,6 +169,7 @@ def create_solid(self): iter_points = iter(self.points) pf_coils_set = [] + wires = [] 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, @@ -180,15 +181,22 @@ def create_solid(self): .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) ) + + wire = solid.close() + + wires.append(wire) + + solid = wire.revolve(self.rotation_angle) + pf_coils_set.append(solid) compound = cq.Compound.makeCompound( [a.val() for a in pf_coils_set] ) + self.wire = wires + 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 index 3be5ba3c7..366abf78f 100644 --- a/paramak/parametric_components/poloidal_field_coil_case_set_fc.py +++ b/paramak/parametric_components/poloidal_field_coil_case_set_fc.py @@ -91,13 +91,17 @@ def find_points(self): 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") + raise ValueError( + "The number pf_coils is not equal to the " + "number of thichnesses provided. " + "casing_thicknesses=", + self.casing_thicknesses, + "num_of_coils=", + num_of_coils) casing_thicknesses_list = self.casing_thicknesses else: casing_thicknesses_list = [self.casing_thicknesses] * num_of_coils @@ -166,6 +170,7 @@ def create_solid(self): iter_points = iter(self.points) pf_coils_set = [] + wires = [] 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, @@ -177,15 +182,22 @@ def create_solid(self): .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) ) + + wire = solid.close() + + wires.append(wire) + + solid = wire.revolve(self.rotation_angle) + pf_coils_set.append(solid) compound = cq.Compound.makeCompound( [a.val() for a in pf_coils_set] ) + self.wire = wires + self.solid = compound return compound diff --git a/paramak/parametric_components/poloidal_field_coil_set.py b/paramak/parametric_components/poloidal_field_coil_set.py index 33b743f11..7e72c82a8 100644 --- a/paramak/parametric_components/poloidal_field_coil_set.py +++ b/paramak/parametric_components/poloidal_field_coil_set.py @@ -124,21 +124,29 @@ def create_solid(self): iter_points = iter(self.points) pf_coils_set = [] + wires = [] 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) ) + + wire = solid.close() + + wires.append(wire) + + solid = wire.revolve(self.rotation_angle) + pf_coils_set.append(solid) compound = cq.Compound.makeCompound( [a.val() for a in pf_coils_set] ) + self.wire = wires + self.solid = compound return compound diff --git a/paramak/parametric_components/poloidal_segmenter.py b/paramak/parametric_components/poloidal_segmenter.py index a3eb2033e..1939e1b62 100644 --- a/paramak/parametric_components/poloidal_segmenter.py +++ b/paramak/parametric_components/poloidal_segmenter.py @@ -146,16 +146,24 @@ def create_solid(self): iter_points = iter(self.points) triangle_wedges = [] + wires = [] 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) ) + + wire = solid.close() + + wires.append(wire) + + solid = wire.revolve(self.rotation_angle) + triangle_wedges.append(solid) + self.wire = wires + if self.shape_to_segment is None: compound = cq.Compound.makeCompound( diff --git a/paramak/parametric_components/toroidal_field_coil_coat_hanger.py b/paramak/parametric_components/toroidal_field_coil_coat_hanger.py index 2e2af7ecb..68dccabff 100644 --- a/paramak/parametric_components/toroidal_field_coil_coat_hanger.py +++ b/paramak/parametric_components/toroidal_field_coil_coat_hanger.py @@ -233,10 +233,14 @@ def create_solid(self): solid = ( cq.Workplane(self.workplane) .polyline(points_without_connections) - .close() - .extrude(distance=-self.distance / 2.0, both=True) ) + wire = solid.close() + + self.wire = wire + + solid = wire.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( diff --git a/paramak/parametric_components/toroidal_field_coil_rectangle.py b/paramak/parametric_components/toroidal_field_coil_rectangle.py index 2e0004882..b8455bb3a 100644 --- a/paramak/parametric_components/toroidal_field_coil_rectangle.py +++ b/paramak/parametric_components/toroidal_field_coil_rectangle.py @@ -132,10 +132,14 @@ def create_solid(self): solid = ( cq.Workplane(self.workplane) .polyline(points_without_connections) - .close() - .extrude(distance=-self.distance / 2.0, both=True) ) + wire = solid.close() + + self.wire = wire + + solid = wire.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( diff --git a/paramak/parametric_neutronics/make_faceteted_neutronics_model.py b/paramak/parametric_neutronics/make_faceteted_neutronics_model.py index 04b16b421..c74059df1 100644 --- a/paramak/parametric_neutronics/make_faceteted_neutronics_model.py +++ b/paramak/parametric_neutronics/make_faceteted_neutronics_model.py @@ -109,9 +109,9 @@ def find_all_surfaces_of_reflecting_wedge(new_vols): #area = surface.area() vertex_in_surface = cubit.parse_cubit_list("vertex", " in surface " + str(surface_id)) if surface.is_planar() == True and len(vertex_in_surface) == 4: - surface_info_dict[surface_id] = {'reflector': True} + surface_info_dict[surface_id] = {'reflector': True} else: - surface_info_dict[surface_id] = {'reflector': False} + surface_info_dict[surface_id] = {'reflector': False} print('surface_info_dict', surface_info_dict) return surface_info_dict diff --git a/paramak/parametric_neutronics/neutronics_model.py b/paramak/parametric_neutronics/neutronics_model.py index 447d1aaf1..45cd1e96f 100644 --- a/paramak/parametric_neutronics/neutronics_model.py +++ b/paramak/parametric_neutronics/neutronics_model.py @@ -11,6 +11,7 @@ try: import openmc + from openmc.data import REACTION_NAME, REACTION_MT except ImportError: warnings.warn('OpenMC not found, NeutronicsModelFromReactor.simulate \ method not available', UserWarning) @@ -48,8 +49,10 @@ class NeutronicsModel(): geometry (paramak.Shape, paramak.Rector): The geometry to convert to a neutronics model. e.g. geometry=paramak.RotateMixedShape() or reactor=paramak.BallReactor() . - cell_tallies (list of strings): the cell based tallies to calculate, - options include TBR, heating and flux + cell_tallies (list of string or int): the cell based tallies to calculate, + options include TBR, heating, flux, MT numbers and OpenMC standard + scores such as (n,Xa) which is helium production are also supported + https://docs.openmc.org/en/latest/usersguide/tallies.html#scores 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 @@ -57,9 +60,13 @@ class NeutronicsModel(): geometry object must be accounted for. Material tags required for a Reactor or Shape can be obtained with .material_tags. mesh_tally_2d (list of str): the 2D mesh based tallies to calculate, - options include tritium_production, heating and flux. + options include heating and flux , MT numbers and OpenMC standard + scores such as (n,Xa) which is helium production are also supported + https://docs.openmc.org/en/latest/usersguide/tallies.html#scores mesh_tally_3d (list of str): the 3D mesh based tallies to calculate, - options include tritium_production, heating and flux. + options include heating and flux , MT numbers and OpenMC standard + scores such as (n,Xa) which is helium production are also supported + https://docs.openmc.org/en/latest/usersguide/tallies.html#scores 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 @@ -162,7 +169,8 @@ def cell_tallies(self, value): raise TypeError( "NeutronicsModelFromReactor.cell_tallies should be a\ list") - output_options = ['TBR', 'heating', 'flux', 'spectra', 'dose'] + output_options = ['TBR', 'heating', 'flux', 'spectra'] + \ + list(REACTION_MT.keys()) + list(REACTION_NAME.keys()) for entry in value: if entry not in output_options: raise ValueError( @@ -183,8 +191,8 @@ def mesh_tally_2d(self, value): raise TypeError( "NeutronicsModelFromReactor.mesh_tally_2d should be a\ list") - output_options = ['tritium_production', 'heating', 'flux', - 'fast flux', 'dose'] + output_options = ['heating', 'flux'] + \ + list(REACTION_MT.keys()) + list(REACTION_NAME.keys()) for entry in value: if entry not in output_options: raise ValueError( @@ -205,8 +213,8 @@ def mesh_tally_3d(self, value): raise TypeError( "NeutronicsModelFromReactor.mesh_tally_3d should be a\ list") - output_options = ['tritium_production', 'heating', 'flux', - 'fast flux', 'dose'] + output_options = ['heating', 'flux'] + \ + list(REACTION_MT.keys()) + list(REACTION_NAME.keys()) for entry in value: if entry not in output_options: raise ValueError( @@ -444,13 +452,8 @@ def create_neutronics_model(self, method: str = None): ] for standard_tally in self.mesh_tally_3d: - if standard_tally == 'tritium_production': - score = '(n,Xt)' # where X is a wild card - prefix = 'tritium_production' - else: - score = standard_tally - prefix = standard_tally - + score = standard_tally + prefix = standard_tally mesh_filter = openmc.MeshFilter(mesh_xyz) tally = openmc.Tally(name=prefix + '_on_3D_mesh') tally.filters = [mesh_filter] @@ -519,12 +522,8 @@ def create_neutronics_model(self, method: str = None): ] for standard_tally in self.mesh_tally_2d: - if standard_tally == 'tritium_production': - score = '(n,Xt)' # where X is a wild card - prefix = 'tritium_production' - else: - score = standard_tally - prefix = standard_tally + score = standard_tally + prefix = standard_tally for mesh_filter, plane in zip( [mesh_xz, mesh_xy, mesh_yz], ['xz', 'xy', 'yz']): @@ -592,7 +591,8 @@ def _add_tally_for_every_material(self, sufix: str, score: str, self.tallies.append(tally) def simulate(self, verbose: bool = True, method: str = None, - cell_tally_results_filename: str = 'results.json'): + cell_tally_results_filename: str = 'results.json', + threads: int = None): """Run the OpenMC simulation. Deletes exisiting simulation output (summary.h5) if files exists. @@ -603,6 +603,9 @@ def simulate(self, verbose: bool = True, method: str = None, method (str): The method to use when making the imprinted and merged geometry. Options are "ppp", "trelis", "pymoab". Defaults to pymoab. + threads (int, optional): Sets the number of OpenMP threads + used for the simulation. None takes all available threads by + default. Defaults to None. Returns: dict: the simulation output filename @@ -621,7 +624,9 @@ def simulate(self, verbose: bool = True, method: str = None, os.system('rm settings.xml') os.system('rm tallies.xml') - self.statepoint_filename = self.model.run(output=verbose) + self.statepoint_filename = self.model.run( + output=verbose, threads=threads + ) self.results = get_neutronics_results_from_statepoint_file( statepoint_filename=self.statepoint_filename, fusion_power=self.fusion_power diff --git a/paramak/parametric_shapes/extruded_circle_shape.py b/paramak/parametric_shapes/extruded_circle_shape.py index ff23a8e72..34cc3bdfd 100644 --- a/paramak/parametric_shapes/extruded_circle_shape.py +++ b/paramak/parametric_shapes/extruded_circle_shape.py @@ -1,4 +1,6 @@ +from typing import Optional + import cadquery as cq from paramak import Shape from paramak.utils import calculate_wedge_cut @@ -8,26 +10,26 @@ 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 + distance: 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". + radius: radius of the shape. + rotation_angle: rotation_angle of solid created. a cut is performed + from rotation_angle to 360 degrees. Defaults to 360. + extrude_both: if set to True, the extrusion will occur in both + directions. Defaults to True. + stp_filename: Defaults to "ExtrudeCircleShape.stp". + stl_filename: 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", + distance: float, + radius: float, + extrusion_start_offset: Optional[float] = 0.0, + rotation_angle: Optional[float] = 360, + extrude_both: Optional[bool] = True, + stp_filename: Optional[str] = "ExtrudeCircleShape.stp", + stl_filename: Optional[str] = "ExtrudeCircleShape.stl", **kwargs ): @@ -76,11 +78,10 @@ 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. + """Creates a extruded 3d solid using points with circular edges. - :return: a 3d solid volume - :rtype: a cadquery solid + Returns: + A CadQuery solid: A 3D solid volume """ # so a positive offset moves extrusion further from axis of azimuthal diff --git a/paramak/parametric_shapes/extruded_mixed_shape.py b/paramak/parametric_shapes/extruded_mixed_shape.py index eadd30731..0e19acc2c 100644 --- a/paramak/parametric_shapes/extruded_mixed_shape.py +++ b/paramak/parametric_shapes/extruded_mixed_shape.py @@ -1,4 +1,6 @@ +from typing import Optional + from paramak import Shape from paramak.utils import calculate_wedge_cut @@ -8,26 +10,25 @@ class ExtrudeMixedShape(Shape): straight and spline connections. Args: - distance (float): the extrusion distance to use (cm units if used for + distance: 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". + extrude_both: If set to True, the extrusion will occur in both + directions. Defaults to True. + rotation_angle: rotation angle of solid created. A cut is performed + from rotation_angle to 360 degrees. Defaults to 360.0. + stp_filename: Defaults to "ExtrudeMixedShape.stp". + stl_filename: 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", + distance: float, + extrude_both: Optional[bool] = True, + rotation_angle: Optional[float] = 360.0, + extrusion_start_offset: Optional[float] = 0.0, + stp_filename: Optional[str] = "ExtrudeMixedShape.stp", + stl_filename: Optional[str] = "ExtrudeMixedShape.stl", **kwargs ): diff --git a/paramak/parametric_shapes/extruded_spline_shape.py b/paramak/parametric_shapes/extruded_spline_shape.py index 016640c6e..da9af7941 100644 --- a/paramak/parametric_shapes/extruded_spline_shape.py +++ b/paramak/parametric_shapes/extruded_spline_shape.py @@ -1,4 +1,6 @@ +from typing import Optional + from paramak import ExtrudeMixedShape @@ -7,17 +9,17 @@ class ExtrudeSplineShape(ExtrudeMixedShape): connections. Args: - distance (float): the extrusion distance to use (cm units if used for + distance: 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". + stp_filename: Defaults to "ExtrudeSplineShape.stp". + stl_filename: Defaults to "ExtrudeSplineShape.stl". """ def __init__( self, - distance, - stp_filename="ExtrudeSplineShape.stp", - stl_filename="ExtrudeSplineShape.stl", + distance: float, + stp_filename: Optional[str] = "ExtrudeSplineShape.stp", + stl_filename: Optional[str] = "ExtrudeSplineShape.stl", **kwargs ): diff --git a/paramak/parametric_shapes/extruded_straight_shape.py b/paramak/parametric_shapes/extruded_straight_shape.py index 423937541..feb65d3c3 100644 --- a/paramak/parametric_shapes/extruded_straight_shape.py +++ b/paramak/parametric_shapes/extruded_straight_shape.py @@ -1,4 +1,6 @@ +from typing import Optional + from paramak import ExtrudeMixedShape @@ -6,17 +8,17 @@ 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 + distance: 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". + stp_filename: Defaults to "ExtrudeStraightShape.stp". + stl_filename: Defaults to "ExtrudeStraightShape.stl". """ def __init__( self, - distance, - stp_filename="ExtrudeStraightShape.stp", - stl_filename="ExtrudeStraightShape.stl", + distance: float, + stp_filename: Optional[str] = "ExtrudeStraightShape.stp", + stl_filename: Optional[str] = "ExtrudeStraightShape.stl", **kwargs ): diff --git a/paramak/parametric_shapes/rotate_circle_shape.py b/paramak/parametric_shapes/rotate_circle_shape.py index 911b89abc..154a772c3 100644 --- a/paramak/parametric_shapes/rotate_circle_shape.py +++ b/paramak/parametric_shapes/rotate_circle_shape.py @@ -1,4 +1,6 @@ +from typing import Optional + import cadquery as cq from paramak import Shape @@ -7,19 +9,19 @@ 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". + radius: radius of the shape + rotation_angle: The rotation_angle to use when revolving the solid + (degrees). Defaults to 360.0. + stp_filename: Defaults to "RotateCircleShape.stp". + stl_filename: Defaults to "RotateCircleShape.stl". """ def __init__( self, - radius, - rotation_angle=360.0, - stp_filename="RotateCircleShape.stp", - stl_filename="RotateCircleShape.stl", + radius: float, + rotation_angle: Optional[float] = 360.0, + stp_filename: Optional[str] = "RotateCircleShape.stp", + stl_filename: Optional[str] = "RotateCircleShape.stl", **kwargs ): diff --git a/paramak/parametric_shapes/rotate_mixed_shape.py b/paramak/parametric_shapes/rotate_mixed_shape.py index 5d3e9a64e..700cf636d 100644 --- a/paramak/parametric_shapes/rotate_mixed_shape.py +++ b/paramak/parametric_shapes/rotate_mixed_shape.py @@ -1,4 +1,6 @@ +from typing import Optional + from paramak import Shape @@ -7,17 +9,17 @@ class RotateMixedShape(Shape): 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". + rotation_angle: The rotation_angle to use when revolving the solid + (degrees). Defaults to 360.0. + stp_filename: Defaults to "RotateMixedShape.stp". + stl_filename: Defaults to "RotateMixedShape.stl". """ def __init__( self, - rotation_angle=360.0, - stp_filename="RotateMixedShape.stp", - stl_filename="RotateMixedShape.stl", + rotation_angle: Optional[float] = 360.0, + stp_filename: Optional[str] = "RotateMixedShape.stp", + stl_filename: Optional[str] = "RotateMixedShape.stl", **kwargs ): diff --git a/paramak/parametric_shapes/rotate_spline_shape.py b/paramak/parametric_shapes/rotate_spline_shape.py index a5f71315c..e152d4e2a 100644 --- a/paramak/parametric_shapes/rotate_spline_shape.py +++ b/paramak/parametric_shapes/rotate_spline_shape.py @@ -1,4 +1,6 @@ +from typing import Optional + from paramak import RotateMixedShape @@ -6,17 +8,17 @@ 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". + rotation_angle: The rotation_angle to use when revolving the solid. + Defaults to 360.0. + stp_filename: Defaults to "RotateSplineShape.stp". + stl_filename: Defaults to "RotateSplineShape.stl". """ def __init__( self, - rotation_angle=360, - stp_filename="RotateSplineShape.stp", - stl_filename="RotateSplineShape.stl", + rotation_angle: Optional[float] = 360, + stp_filename: Optional[str] = "RotateSplineShape.stp", + stl_filename: Optional[str] = "RotateSplineShape.stl", **kwargs ): diff --git a/paramak/parametric_shapes/rotate_straight_shape.py b/paramak/parametric_shapes/rotate_straight_shape.py index 184b899fc..50db7dd16 100644 --- a/paramak/parametric_shapes/rotate_straight_shape.py +++ b/paramak/parametric_shapes/rotate_straight_shape.py @@ -1,4 +1,6 @@ +from typing import Optional + from paramak import RotateMixedShape @@ -7,17 +9,17 @@ class RotateStraightShape(RotateMixedShape): 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". + rotation_angle: The rotation angle to use when revolving the solid + (degrees). + stp_filename: Defaults to "RotateStraightShape.stp". + stl_filename: Defaults to "RotateStraightShape.stl". """ def __init__( self, - rotation_angle=360.0, - stp_filename="RotateStraightShape.stp", - stl_filename="RotateStraightShape.stl", + rotation_angle: Optional[float] = 360.0, + stp_filename: Optional[str] = "RotateStraightShape.stp", + stl_filename: Optional[str] = "RotateStraightShape.stl", **kwargs ): diff --git a/paramak/parametric_shapes/sweep_circle_shape.py b/paramak/parametric_shapes/sweep_circle_shape.py index 64a2c2766..49126726d 100644 --- a/paramak/parametric_shapes/sweep_circle_shape.py +++ b/paramak/parametric_shapes/sweep_circle_shape.py @@ -1,4 +1,6 @@ +from typing import Optional, List, Tuple + import cadquery as cq from paramak import Shape @@ -9,30 +11,29 @@ class SweepCircleShape(Shape): 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 + radius: Radius of 2D circle to be swept. + path_points: A list of XY, YZ or XZ coordinates connected by spline + connections which define the path along which the 2D shape is swept + workplane: Workplane in which the circle to be swept is defined. Defaults to "XY". - path_workplane (str, optional): Workplane in which the spline path is + path_workplane: 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. + stp_filename: Defaults to "SweepCircleShape.stp". + stl_filename: Defaults to "SweepCircleShape.stl". + force_cross_section: 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, + radius: float, + path_points: List[Tuple[float, float]], + workplane: Optional[str] = "XY", + path_workplane: Optional[str] = "XZ", + stp_filename: Optional[str] = "SweepCircleShape.stp", + stl_filename: Optional[str] = "SweepCircleShape.stl", + force_cross_section: Optional[bool] = False, **kwargs ): @@ -95,19 +96,30 @@ def create_solid(self): if self.workplane in ["XZ", "YX", "ZY"]: factor *= -1 + wires = [] if self.force_cross_section: wire = cq.Workplane(self.workplane).center(0, 0) for point in self.path_points[:-1]: - wire = wire.workplane(offset=point[1] * factor).\ - center(point[0], 0).\ - circle(self.radius).\ - center(-point[0], 0).\ - workplane(offset=-point[1] * factor) - - self.wire = wire - - solid = wire.workplane(offset=self.path_points[-1][1] * factor).center( - self.path_points[-1][0], 0).circle(self.radius).sweep(path, multisection=True) + wire = ( + wire.workplane(offset=point[1] * factor) + .center(point[0], 0) + .circle(self.radius) + ) + + wires.append(wire) + + wire = ( + wire.center(-point[0], 0) + .workplane(offset=-point[1] * factor) + ) + + self.wire = wires + + solid = wire.workplane( + offset=self.path_points[-1][1] * factor) \ + .center(self.path_points[-1][0], 0) \ + .circle(self.radius) \ + .sweep(path, multisection=True) else: diff --git a/paramak/parametric_shapes/sweep_mixed_shape.py b/paramak/parametric_shapes/sweep_mixed_shape.py index 135ac2a83..43ea62777 100644 --- a/paramak/parametric_shapes/sweep_mixed_shape.py +++ b/paramak/parametric_shapes/sweep_mixed_shape.py @@ -1,4 +1,6 @@ +from typing import Optional, List, Tuple + import cadquery as cq from paramak import Shape @@ -9,15 +11,15 @@ class SweepMixedShape(Shape): 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". + path_points: A list of XY, YZ or XZ coordinates connected by spline + connections which define the path along which the 2D shape is + swept. + workplane: Workplane in which the 2D shape to be swept is defined. + Defaults to "XY". + path_workplane: Workplane in which the spline path is defined. Defaults + to "XZ". + stp_filename: Defaults to "SweepMixedShape.stp". + stl_filename: 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. @@ -25,12 +27,12 @@ class SweepMixedShape(Shape): def __init__( self, - path_points, - workplane="XY", - path_workplane="XZ", - stp_filename="SweepMixedShape.stp", - stl_filename="SweepMixedShape.stl", - force_cross_section=False, + path_points: List[Tuple[float, float]], + workplane: Optional[str] = "XY", + path_workplane: Optional[str] = "XZ", + stp_filename: Optional[str] = "SweepMixedShape.stp", + stl_filename: Optional[str] = "SweepMixedShape.stl", + force_cross_section: Optional[bool] = False, **kwargs ): diff --git a/paramak/parametric_shapes/sweep_spline_shape.py b/paramak/parametric_shapes/sweep_spline_shape.py index 0605bf04b..228ccbde0 100644 --- a/paramak/parametric_shapes/sweep_spline_shape.py +++ b/paramak/parametric_shapes/sweep_spline_shape.py @@ -1,4 +1,6 @@ +from typing import List, Optional, Tuple + from paramak import SweepMixedShape @@ -8,15 +10,15 @@ class SweepSplineShape(SweepMixedShape): 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". + path_points: A list of XY, YZ or XZ coordinates connected by spline + connections which define the path along which the 2D shape is + swept. + workplane: Workplane in which the 2D shape to be swept is defined. + Defaults to "XY". + path_workplane: Workplane in which the spline path is defined. Defaults + to "XZ". + stp_filename: Defaults to "SweepSplineShape.stp". + stl_filename: 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. @@ -24,12 +26,12 @@ class SweepSplineShape(SweepMixedShape): def __init__( self, - path_points, - workplane="XY", - path_workplane="XZ", - stp_filename="SweepSplineShape.stp", - stl_filename="SweepSplineShape.stl", - force_cross_section=False, + path_points: List[Tuple[float, float]], + workplane: Optional[str] = "XY", + path_workplane: Optional[str] = "XZ", + stp_filename: Optional[str] = "SweepSplineShape.stp", + stl_filename: Optional[str] = "SweepSplineShape.stl", + force_cross_section: Optional[bool] = False, **kwargs ): diff --git a/paramak/parametric_shapes/sweep_straight_shape.py b/paramak/parametric_shapes/sweep_straight_shape.py index 0464806d5..e43c085b5 100644 --- a/paramak/parametric_shapes/sweep_straight_shape.py +++ b/paramak/parametric_shapes/sweep_straight_shape.py @@ -1,4 +1,6 @@ +from typing import Optional, List, Tuple + from paramak import SweepMixedShape @@ -8,28 +10,27 @@ class SweepStraightShape(SweepMixedShape): 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. + path_points: A list of XY, YZ or XZ coordinates connected by spline + connections which define the path along which the 2D shape is swept + workplane: Workplane in which the 2D shape to be swept is defined. + Defaults to "XY". + path_workplane: Workplane in which the spline path is defined. Defaults + to "XZ". + stp_filename: Defaults to "SweepStraightShape.stp". + stl_filename: Defaults to "SweepStraightShape.stl". + force_cross_section: 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, + path_points: List[Tuple[float, float]], + workplane: Optional[str] = "XY", + path_workplane: Optional[str] = "XZ", + stp_filename: Optional[str] = "SweepStraightShape.stp", + stl_filename: Optional[str] = "SweepStraightShape.stl", + force_cross_section: Optional[bool] = False, **kwargs ): diff --git a/paramak/reactor.py b/paramak/reactor.py index 6e61d5339..91c202608 100644 --- a/paramak/reactor.py +++ b/paramak/reactor.py @@ -1,11 +1,11 @@ import json -from collections import Iterable +from collections.abc import Iterable from pathlib import Path +from typing import Optional, Tuple, List import cadquery as cq import matplotlib.pyplot as plt -import plotly.graph_objects as go from cadquery import exporters import paramak @@ -82,11 +82,11 @@ def material_tags(self): (excluding the plasma)""" values = [] for shape_or_component in self.shapes_and_components: - if isinstance( + if not isinstance( shape_or_component, (paramak.Plasma, paramak.PlasmaFromPoints, - paramak.PlasmaBoundaries)) is False: + paramak.PlasmaBoundaries)): values.append(shape_or_component.material_tag) return values @@ -97,8 +97,8 @@ def material_tags(self, value): @property def tet_meshes(self): values = [] - for shape_or_componet in self.shapes_and_components: - values.append(shape_or_componet.tet_mesh) + for shape_or_component in self.shapes_and_components: + values.append(shape_or_component.tet_mesh) return values @tet_meshes.setter @@ -141,8 +141,7 @@ def graveyard_offset(self, 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. + object. """ list_of_cq_vals = [] @@ -150,7 +149,7 @@ def solid(self): for shape_or_compound in self.shapes_and_components: if isinstance( shape_or_compound.solid, - cq.occ_impl.shapes.Compound): + (cq.occ_impl.shapes.Shape, cq.occ_impl.shapes.Compound)): for solid in shape_or_compound.solid.Solids(): list_of_cq_vals.append(solid) else: @@ -158,26 +157,24 @@ def solid(self): 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 + @ solid.setter def solid(self, value): self._solid = value - def neutronics_description(self, include_plasma: bool = False, - include_graveyard: bool = True - ): + def neutronics_description( + self, + include_plasma: Optional[bool] = False, + include_graveyard: Optional[bool] = True) -> dict: """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. + 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 @@ -220,9 +217,9 @@ def neutronics_description(self, include_plasma: bool = False, def export_neutronics_description( self, - filename: str = "manifest.json", - include_plasma: bool = False, - include_graveyard: bool = True) -> str: + filename: Optional[str] = "manifest.json", + include_plasma: Optional[bool] = False, + include_graveyard: Optional[bool] = True) -> str: """ Saves Reactor.neutronics_description to a json file. The resulting json file contains a list of dictionaries. Each dictionary entry comprises @@ -271,9 +268,9 @@ def export_neutronics_description( def export_stp( self, - output_folder: str = "", - graveyard_offset: float = 100, - mode: str = 'solid') -> list: + output_folder: Optional[str] = "", + graveyard_offset: Optional[float] = 100, + mode: Optional[str] = 'solid') -> List[str]: """Writes stp files (CAD geometry) for each Shape object in the reactor and the graveyard. @@ -311,7 +308,7 @@ def export_stp( ) # creates a graveyard (bounding shell volume) which is needed for - # nuetronics simulations + # neutronics simulations self.make_graveyard(graveyard_offset=graveyard_offset) filenames.append( str(Path(output_folder) / Path(self.graveyard.stp_filename))) @@ -321,17 +318,22 @@ def export_stp( return filenames - def export_stl(self, output_folder: str = "", - tolerance: float = 0.001) -> list: + def export_stl( + self, + output_folder: Optional[str] = "", + graveyard_offset: Optional[float] = 100, + tolerance: Optional[float] = 0.001) -> List[str]: """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 + Args: + output_folder (str): the folder for saving the stl files to + graveyard_offset (float, optional): the offset between the largest + edge of the geometry and inner bounding shell created. Defaults + to 100. + tolerance (float): the precision of the faceting - :return: a list of stl filenames created - :rtype: list + Returns: + list: a list of stl filenames created """ if len(self.stl_filenames) != len(set(self.stl_filenames)): @@ -359,8 +361,8 @@ def export_stl(self, output_folder: str = "", tolerance) # creates a graveyard (bounding shell volume) which is needed for - # nuetronics simulations - self.make_graveyard() + # neutronics simulations + self.make_graveyard(graveyard_offset=graveyard_offset) filenames.append( str(Path(output_folder) / Path(self.graveyard.stl_filename))) self.graveyard.export_stl( @@ -373,10 +375,10 @@ def export_stl(self, output_folder: str = "", def export_h5m( self, - filename: str = 'dagmc.h5m', - skip_graveyard: bool = False, - tolerance: float = 0.001, - graveyard_offset: float = 100) -> str: + filename: Optional[str] = 'dagmc.h5m', + skip_graveyard: Optional[bool] = False, + tolerance: Optional[float] = 0.001, + graveyard_offset: Optional[float] = 100) -> str: """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 @@ -446,7 +448,9 @@ def export_h5m( return str(path_filename) - def export_physical_groups(self, output_folder: str = "") -> list: + def export_physical_groups( + self, + output_folder: Optional[str] = "") -> List[str]: """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. @@ -474,13 +478,50 @@ def export_physical_groups(self, output_folder: str = "") -> list: Path(output_folder) / Path(entry.stp_filename)) return filenames - def export_svg(self, filename: str = 'reactor.svg') -> str: + def export_svg( + self, + filename: Optional[str] = 'reactor.svg', + projectionDir: Tuple[float, float, float] = (-1.75, 1.1, 5), + width: Optional[float] = 1000, + height: Optional[float] = 800, + marginLeft: Optional[float] = 120, + marginTop: Optional[float] = 100, + strokeWidth: Optional[float] = None, + strokeColor: Optional[Tuple[int, int, int]] = (0, 0, 0), + hiddenColor: Optional[Tuple[int, int, int]] = (100, 100, 100), + showHidden: Optional[bool] = True, + showAxes: Optional[bool] = False) -> str: """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. - Defaults to "reactor.svg". + filename: the filename of the svg file to be exported. Defaults to + "reactor.svg". + projectionDir: The direction vector to view the geometry from + (x, y, z). Defaults to (-1.75, 1.1, 5) + width: the width of the svg image produced in pixels. Defaults to + 1000 + height: the height of the svg image produced in pixels. Defaults to + 800 + marginLeft: the number of pixels between the left edge of the image + and the start of the geometry. + marginTop: the number of pixels between the top edge of the image + and the start of the geometry. + strokeWidth: the width of the lines used to draw the geometry. + Defaults to None which automatically selects an suitable width. + strokeColor: the color of the lines used to draw the geometry in + RGB format with each value between 0 and 255. Defaults to + (0, 0, 0) which is black. + hiddenColor: the color of the lines used to draw the geometry in + RGB format with each value between 0 and 255. Defaults to + (100, 100, 100) which is light grey. + showHidden: If the edges obscured by geometry should be included in + the diagram. Defaults to True. + showAxes: If the x, y, z axis should be included in the image. + Defaults to False. + + Returns: + str: the svg filename created """ path_filename = Path(filename) @@ -490,16 +531,32 @@ def export_svg(self, filename: str = 'reactor.svg') -> str: 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) + opt = { + "width": width, + "height": height, + "marginLeft": marginLeft, + "marginTop": marginTop, + "showAxes": showAxes, + "projectionDir": projectionDir, + "strokeColor": strokeColor, + "hiddenColor": hiddenColor, + "showHidden": showHidden + } + + if strokeWidth is not None: + opt["strokeWidth"] = strokeWidth + + exporters.export(self.solid, str(path_filename), exportType='SVG', + opt=opt) + print("Saved file as ", path_filename) return str(path_filename) def export_graveyard( self, - graveyard_offset: float = 100, - filename: str = "Graveyard.stp"): + graveyard_offset: Optional[float] = 100, + filename: Optional[str] = "Graveyard.stp") -> str: """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. @@ -519,7 +576,9 @@ def export_graveyard( return new_filename - def make_graveyard(self, graveyard_offset: float = 100): + def make_graveyard( + self, + graveyard_offset: Optional[float] = 100) -> paramak.Shape: """Creates a graveyard volume (bounding box) that encapsulates all volumes. This is required by DAGMC when performing neutronics simulations. @@ -554,11 +613,11 @@ def make_graveyard(self, graveyard_offset: float = 100): def export_2d_image( self, - filename="2d_slice.png", - xmin: float = 0.0, - xmax: float = 900.0, - ymin: float = -600.0, - ymax: float = 600.0) -> str: + filename: Optional[str] = "2d_slice.png", + xmin: Optional[float] = 0.0, + xmax: Optional[float] = 900.0, + ymin: Optional[float] = -600.0, + ymax: Optional[float] = 600.0) -> str: """Creates a 2D slice image (png) of the reactor. Args: @@ -594,35 +653,45 @@ def export_2d_image( return str(path_filename) - def export_html(self, filename="reactor.html"): + def export_html( + self, + filename: Optional[str] = "reactor.html", + view_plane: Optional[str] = 'RZ'): """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. + objects that make up the reactor. 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. Viewed from the XZ + plane Args: - filename (str): the filename to save the html graph - + filename: the filename used to save the html graph. Defaults to + reactor.html + view_plane: The axis to view the points and faceted edges from. The + options are 'XZ', 'XY', 'YZ', 'YX', 'ZY', 'ZX', 'RZ'. Defaults + to 'RZ'. Returns: - plotly figure: figure object + 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 + # accesses the Shape wires for each Shape and builds up a list of + # traces + all_wires = [] 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)) + if not isinstance(entry.wire, list): + list_of_wires = [entry.wire] + else: + list_of_wires = entry.wire + all_wires = all_wires + list_of_wires + + fig = paramak.utils.export_wire_to_html( + wires=all_wires, + filename=filename, + view_plane=view_plane, + facet_splines=True, + facet_circles=True, + tolerance=1e-3, + title="coordinates of the " + self.__class__.__name__ + + " reactor, viewed from the " + view_plane + " plane", + ) return fig diff --git a/paramak/shape.py b/paramak/shape.py index 2711f8c5f..cda6a7b9c 100644 --- a/paramak/shape.py +++ b/paramak/shape.py @@ -2,12 +2,12 @@ import json import numbers import warnings -from collections import Iterable +from collections.abc import Iterable from pathlib import Path +from typing import List, Tuple, Optional, Union 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 @@ -15,14 +15,14 @@ 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, - _replace) +from paramak.utils import (_replace, cut_solid, facet_wire, get_hash, + intersect_solid, plotly_trace, 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 + directly by the user but provide basic functionality for user-facing classes that inherit from Shape. Args: @@ -33,9 +33,9 @@ class Shape: 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). + 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. @@ -74,18 +74,19 @@ class Shape: def __init__( self, points: list = None, - connection_type="mixed", - name=None, - color=(0.5, 0.5, 0.5), - material_tag: str = None, - stp_filename: str = None, - stl_filename: str = None, - azimuth_placement_angle=0.0, - workplane: str = "XZ", - rotation_axis=None, - tet_mesh: str = None, - surface_reflectivity: bool = False, + connection_type: Optional[str] = "mixed", + name: Optional[str] = None, + color: Optional[Tuple[float, float, float]] = (0.5, 0.5, 0.5), + material_tag: Optional[str] = None, + stp_filename: Optional[str] = None, + stl_filename: Optional[str] = None, + azimuth_placement_angle: Optional[Union[float, List[float]]] = 0.0, + workplane: Optional[str] = "XZ", + rotation_axis: Optional[str] = None, + tet_mesh: Optional[str] = None, + surface_reflectivity: Optional[bool] = False, physical_groups=None, + # TODO defining Shape types as paramak.Shape results in circular import cut=None, intersect=None, union=None, @@ -362,7 +363,7 @@ def material_tag(self, value): if len(value) > 27: msg = "Shape.material_tag > 28 characters." + \ "Use with DAGMC will be affected." + str(value) - warnings.warn(msg, UserWarning) + warnings.warn(msg) self._material_tag = value else: raise ValueError("Shape.material_tag must be a string", value) @@ -415,6 +416,9 @@ def points(self, values): if not isinstance(values, list): raise ValueError("points must be a list") + if self.connection_type != "mixed": + values = [(*p, self.connection_type) for p in values] + for value in values: if type(value) not in [list, tuple]: msg = "individual points must be a list or a tuple." + \ @@ -463,8 +467,6 @@ def points(self, values): raise ValueError(msg) values.append(values[0]) - if self.connection_type != "mixed": - values = [(*p, self.connection_type) for p in values] self._points = values @@ -546,12 +548,18 @@ def azimuth_placement_angle(self, value): raise ValueError(msg) self._azimuth_placement_angle = value - def create_solid(self): + def create_solid(self) -> cq.Workplane: 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] + for point in self.points: + if len(point) != 3: + msg = "The points list should contain two coordinates and \ + a connetion type" + raise ValueError(msg) + # obtains the last values of the points list connections = [p[2] for p in self.points[:-1]] @@ -648,7 +656,9 @@ def create_solid(self): return solid - def rotate_solid(self, solid): + def rotate_solid( + self, + solid: Optional[cq.Workplane]) -> cq.Workplane: # 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 @@ -669,6 +679,8 @@ def rotate_solid(self, solid): return solid def get_rotation_axis(self): + # TODO add return type hinting -> Tuple[List[Tuple[int, int, int], + # Tuple[int, int, int]], str] """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 @@ -706,7 +718,7 @@ def get_rotation_axis(self): workplane = self.workplane return rotation_axis[workplane[1]], workplane[1] - def create_limits(self): + def create_limits(self) -> Tuple[float, float, float, float]: """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. @@ -732,13 +744,18 @@ def create_limits(self): return self.x_min, self.x_max, self.z_min, self.z_max - def export_stl(self, filename: str, tolerance: float = 0.001) -> str: + def export_stl( + self, + filename: str, + tolerance: Optional[float] = 0.001, + angular_tolerance: Optional[float] = 0.1) -> str: """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 + filename: the filename of the stl file to be exported + tolerance: the deflection tolerance of the faceting + angular_tolerance: the angular tolerance, in radians """ path_filename = Path(filename) @@ -748,17 +765,19 @@ def export_stl(self, filename: str, tolerance: float = 0.001) -> str: 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) + exporters.export(self.solid, str(path_filename), exportType='STL', + tolerance=tolerance, + angularTolerance=angular_tolerance) + print("Saved file as ", path_filename) return str(path_filename) def export_stp( self, - filename=None, - units='mm', - mode: str = 'solid') -> str: + filename: Optional[str] = None, + units: Optional[str] = 'mm', + mode: Optional[str] = 'solid') -> str: """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 @@ -785,14 +804,13 @@ def export_stp( elif self.stp_filename is not None: path_filename = Path(self.stp_filename) - with open(path_filename, "w") as out_file: - if mode == 'solid': - exporters.exportShape(self.solid, "STEP", out_file) - elif mode == 'wire': - exporters.exportShape(self.wire, "STEP", out_file) - else: - raise ValueError("The mode argument for export_stp \ - only accepts 'solid' or 'wire'", self) + if mode == 'solid': + exporters.export(self.solid, str(path_filename), exportType='STEP') + elif mode == 'wire': + exporters.export(self.wire, str(path_filename), exportType='STEP') + else: + raise ValueError("The mode argument for export_stp \ + only accepts 'solid' or 'wire'", self) if units == 'cm': _replace( @@ -834,12 +852,50 @@ def export_physical_groups(self, filename: str) -> str: return str(path_filename) - def export_svg(self, filename: str) -> str: - """Exports an svg file for the Shape.solid. If the provided filename + def export_svg( + self, + filename: Optional[str] = 'shape.svg', + projectionDir: Tuple[float, float, float] = (-1.75, 1.1, 5), + width: Optional[float] = 800, + height: Optional[float] = 800, + marginLeft: Optional[float] = 100, + marginTop: Optional[float] = 100, + strokeWidth: Optional[float] = None, + strokeColor: Optional[Tuple[int, int, int]] = (0, 0, 0), + hiddenColor: Optional[Tuple[int, int, int]] = (100, 100, 100), + showHidden: Optional[bool] = True, + showAxes: Optional[bool] = False) -> str: + """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 + filename: the filename of the svg file to be exported. Defaults to + "reactor.svg". + projectionDir: The direction vector to view the geometry from + (x, y, z). Defaults to (-1.75, 1.1, 5) + width: the width of the svg image produced in pixels. Defaults to + 1000 + height: the height of the svg image produced in pixels. Defaults to + 800 + marginLeft: the number of pixels between the left edge of the image + and the start of the geometry. + marginTop: the number of pixels between the top edge of the image + and the start of the geometry. + strokeWidth: the width of the lines used to draw the geometry. + Defaults to None which automatically selects an suitable width. + strokeColor: the color of the lines used to draw the geometry in + RGB format with each value between 0 and 255. Defaults to + (0, 0, 0) which is black. + hiddenColor: the color of the lines used to draw the geometry in + RGB format with each value between 0 and 255. Defaults to + (100, 100, 100) which is light grey. + showHidden: If the edges obscured by geometry should be included in + the diagram. Defaults to True. + showAxes: If the x, y, z axis should be included in the image. + Defaults to False. + + Returns: + str: the svg filename created """ path_filename = Path(filename) @@ -849,121 +905,112 @@ def export_svg(self, filename: str) -> str: 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) + opt = { + "width": width, + "height": height, + "marginLeft": marginLeft, + "marginTop": marginTop, + "showAxes": showAxes, + "projectionDir": projectionDir, + "strokeColor": strokeColor, + "hiddenColor": hiddenColor, + "showHidden": showHidden + } + + if strokeWidth is not None: + opt["strokeWidth"] = strokeWidth + + exporters.export(self.solid, str(path_filename), exportType='SVG', + opt=opt) + print("Saved file as ", path_filename) return str(path_filename) - def export_html(self, filename: str): + def export_html( + self, + filename: Optional[str] = "shape.html", + facet_splines: Optional[bool] = True, + facet_circles: Optional[bool] = True, + tolerance: Optional[float] = 1e-3, + view_plane: Optional[str] = None, + ): """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 + filename: the filename used to save the html graph. Defaults to + shape.html + facet_splines: If True then spline edges will be faceted. Defaults + to True. + facet_circles: If True then circle edges will be faceted. Defaults + to True. + tolerance: faceting toleranceto use when faceting cirles and + splines. Defaults to 1e-3. + view_plane: The plane to project Defaults to the workplane of the + paramak.Shape 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 view plane is not set then use the shape workplane + if view_plane is None: + view_plane = self.workplane if self.points is None: - raise ValueError("No points defined for", self) - - Path(filename).parents[0].mkdir(parents=True, exist_ok=True) + if hasattr(self, 'path_points') and self.path_points is None: + raise ValueError("No points or point_path defined for", self) - path_filename = Path(filename) + if self.wire is None: + raise ValueError("No wire defined for", self) - if path_filename.suffix != ".html": - path_filename = path_filename.with_suffix(".html") - - fig = go.Figure() - fig.update_layout( - {"title": "coordinates of components", "hovermode": "closest"} + if not isinstance(self.wire, list): + list_of_wires = [self.wire] + else: + list_of_wires = self.wire + + fig = paramak.utils.export_wire_to_html( + wires=list_of_wires, + filename=filename, + view_plane=view_plane, + facet_splines=facet_splines, + facet_circles=facet_circles, + tolerance=tolerance, + title="coordinates of " + self.__class__.__name__ + + " shape, viewed from the " + view_plane + " plane", ) - 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]) - + "
" + if self.points is not None: + fig.add_trace( + plotly_trace( + points=self.points, + mode="markers", + name='Shape.points' ) + ) - 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, - } - ) + # sweep shapes have .path_points but not .points attribute + if hasattr(self, 'path_points'): + fig.add_trace( + plotly_trace( + points=self.path_points, + mode="markers", + name='Shape.path_points' + ) + ) - return trace + return fig def export_2d_image( - self, filename: str, xmin: float = 0., xmax: float = 900., - ymin: float = -600., ymax: float = 600.): + self, + filename: Optional[str] = 'shape.png', + xmin: Optional[float] = 0., + xmax: Optional[float] = 900., + ymin: Optional[float] = -600., + ymax: Optional[float] = 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. @@ -1014,12 +1061,18 @@ def _create_patch(self): raise ValueError("No points defined for", self) patches = [] - xylist = [] - for point in self.points: - xylist.append([point[0], point[1]]) + edges = facet_wire( + wire=self.wire, + facet_splines=True, + facet_circles=True) + + fpoints = [] + for edge in edges: + for vertice in edge.Vertices(): + fpoints.append((vertice.X, vertice.Z)) - polygon = Polygon(xylist, closed=True) + polygon = Polygon(fpoints, closed=True) patches.append(polygon) patch = PatchCollection(patches) @@ -1064,7 +1117,7 @@ def neutronics_description(self) -> dict: return neutronics_description - def perform_boolean_operations(self, solid, **kwargs): + def perform_boolean_operations(self, solid: cq.Workplane, **kwargs): """Performs boolean cut, intersect and union operations if shapes are provided""" @@ -1089,7 +1142,9 @@ def perform_boolean_operations(self, solid, **kwargs): return solid - def make_graveyard(self, graveyard_offset: int = 100): + def make_graveyard( + self, + graveyard_offset: Optional[int] = 100) -> cq.Workplane: """Creates a graveyard volume (bounding box) that encapsulates all volumes. This is required by DAGMC when performing neutronics simulations. @@ -1123,10 +1178,10 @@ def make_graveyard(self, graveyard_offset: int = 100): def export_h5m( self, - filename: str = 'dagmc.h5m', - skip_graveyard: bool = False, - tolerance: float = 0.001, - graveyard_offset: float = 100) -> str: + filename: Optional[str] = 'dagmc.h5m', + skip_graveyard: Optional[bool] = False, + tolerance: Optional[float] = 0.001, + graveyard_offset: Optional[float] = 100) -> str: """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 @@ -1193,8 +1248,8 @@ def export_h5m( def export_graveyard( self, - graveyard_offset: float = 100, - filename: str = "Graveyard.stp") -> str: + graveyard_offset: Optional[float] = 100, + filename: Optional[str] = "Graveyard.stp") -> str: """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. diff --git a/paramak/utils.py b/paramak/utils.py index 872e2eca4..497b756fb 100644 --- a/paramak/utils.py +++ b/paramak/utils.py @@ -1,18 +1,92 @@ import math -from collections import Iterable +from collections.abc import Iterable from hashlib import blake2b from os import fdopen, remove +from pathlib import Path from shutil import copymode, move from tempfile import mkstemp -from typing import Tuple, List +from typing import List, Tuple, Union import cadquery as cq import numpy as np +import plotly.graph_objects as go +from cadquery import importers +from OCP.GCPnts import GCPnts_QuasiUniformDeflection import paramak +def _transform_curve(edge, tolerance: float = 1e-3): + """Converts a curved edge into a series of straight lines (facetets) with + the provided tolerance. + + Args: + edge (cadquery.Wire): The CadQuery wire to redraw as a series of + straight lines (facet) + tolerance: faceting toleranceto use when faceting cirles and + splines. Defaults to 1e-3. + + Returns: + cadquery.Wire + """ + + curve = edge._geomAdaptor() # adapt the edge into curve + start = curve.FirstParameter() + end = curve.LastParameter() + + points = GCPnts_QuasiUniformDeflection(curve, tolerance, start, end) + verts = (cq.Vector(points.Value(i + 1)) for i in range(points.NbPoints())) + + return cq.Wire.makePolygon(verts) + + +def facet_wire( + wire, + facet_splines: bool = True, + facet_circles: bool = True, + tolerance: float = 1e-3 +): + """Converts specified curved edge types from a wire into a series of + straight lines (facetets) with the provided tol (tolerance). + + Args: + wire (cadquery.Wire): The CadQuery wire to select edge from which will + be redraw as a series of straight lines (facet). + facet_splines: If True then spline edges will be faceted. Defaults + to True. + facet_splines: If True then circle edges will be faceted.Defaults + to True. + tolerance: faceting toleranceto use when faceting cirles and + splines. Defaults to 1e-3. + + Returns: + cadquery.Wire + """ + edges = [] + + types_to_facet = [] + if facet_splines: + types_to_facet.append('BSPLINE') + if facet_circles: + types_to_facet.append('CIRCLE') + + if isinstance(wire, cq.occ_impl.shapes.Wire): + # this is for imported stp files + iterable_of_wires = wire.Edges() + else: + # this is for cadquery generated solids + iterable_of_wires = wire.val().Edges() + + for edge in iterable_of_wires: + if edge.geomType() in types_to_facet: + edges.extend(_transform_curve(edge, tolerance=tolerance).Edges()) + else: + edges.append(edge) + + return edges + + def coefficients_of_line_from_points( point_a: Tuple[float, float], point_b: Tuple[float, float]) -> Tuple[float, float]: """Computes the m and c coefficients of the equation (y=mx+c) for @@ -320,6 +394,266 @@ def _replace(filename: str, pattern: str, subst: str) -> None: move(abs_path, filename) +def plotly_trace( + points: Union[List[Tuple[float, float]], List[Tuple[float, float, float]]], + mode: str = "markers+lines", + name: str = None, + color: Union[Tuple[float, float, float], Tuple[float, float, float, float]] = None +) -> Union[go.Scatter, go.Scatter3d]: + """Creates a plotly trace representation of the points of the Shape + object. This method is intended for internal use by Shape.export_html. + + Args: + points: A list of tuples containing the X, Z points of to add to + the trace. + mode: The mode to use for the Plotly.Scatter graph. Options include + "markers", "lines" and "markers+lines". Defaults to + "markers+lines" + name: The name to use in the graph legend + color + + Returns: + plotly trace: trace object + """ + + if color is not None: + color_list = [i * 255 for i in color] + + if len(color_list) == 3: + color = "rgb(" + str(color_list).strip("[]") + ")" + elif len(color_list) == 4: + color = "rgba(" + str(color_list).strip("[]") + ")" + + if name is None: + name = "Shape not named" + else: + name = name + + text_values = [] + + for i, point in enumerate(points): + text = "point number= {}
x={}
y= {}".format( + i, point[0], point[1]) + if len(point) == 3: + text = text + "
z= {}
".format(point[2]) + + text_values.append(text) + + if all(len(entry) == 3 for entry in points): + trace = go.Scatter3d( + x=[row[0] for row in points], + y=[row[1] for row in points], + z=[row[2] for row in points], + mode=mode, + marker={"size": 3, "color": color}, + name=name + ) + + return trace + + trace = go.Scatter( + x=[row[0] for row in points], + y=[row[1] for row in points], + hoverinfo="text", + text=text_values, + mode=mode, + marker={"size": 5, "color": color}, + name=name, + ) + + return trace + + +def extract_points_from_edges( + edges: Union[List[cq.Wire], cq.Wire], + view_plane: str = 'XZ', +): + """Extracts points (coordinates) from a CadQuery Edge, optionally projects + the points to a plane and returns the points. + + Args: + edges (CadQuery.Wires): The edges to extract points (coordinates from). + view_plane: The axis to view the points and faceted edges from. The + options are 'XZ', 'XY', 'YZ', 'YX', 'ZY', 'ZX', 'RZ' and 'XYZ'. + Defaults to 'RZ'. + + Returns: + List of Tuples: A list of tuples with float entries for every point + """ + + if isinstance(edges, Iterable): + list_of_edges = edges + else: + list_of_edges = [edges] + + points = [] + + for edge in list_of_edges: + for vertex in edge.Vertices(): + if view_plane == 'XZ': + points.append((vertex.X, vertex.Z)) + elif view_plane == 'XY': + points.append((vertex.X, vertex.Y)) + elif view_plane == 'YZ': + points.append((vertex.Y, vertex.Z)) + elif view_plane == 'YX': + points.append((vertex.Y, vertex.X)) + elif view_plane == 'ZY': + points.append((vertex.Z, vertex.Y)) + elif view_plane == 'ZX': + points.append((vertex.Z, vertex.X)) + elif view_plane == 'RZ': + xy_coord = math.pow(vertex.X, 2) + math.pow(vertex.Y, 2) + points.append((math.sqrt(xy_coord), vertex.Z)) + elif view_plane == 'XYZ': + points.append((vertex.X, vertex.Y, vertex.Z)) + else: + raise ValueError('view_plane value of ', view_plane, + ' is not supported') + return points + + +def load_stp_file( + filename: str, + scale_factor: float = 1. +): + """Loads a stp file and makes the 3D solid and wires avaialbe for use. + + Args: + filename: the filename used to save the html graph. + scale_factor: a scaling factor to apply to the geometry that can be + used to increase the size or decrease the size of the geometry. + Useful when converting the geometry to cm for use in neutronics + simulations. + + Returns: + CadQuery.solid, CadQuery.Wires: soild and wires belonging to the object + """ + + part = importers.importStep(str(filename)).val() + + scaled_part = part.scale(scale_factor) + solid = scaled_part + wire = scaled_part.Wires() + return solid, wire + + +def export_wire_to_html( + wires, + filename, + view_plane='RZ', + facet_splines: bool = True, + facet_circles: bool = True, + tolerance: float = 1e-3, + title=None, +): + """Creates a html graph representation of the points within the wires. + Edges of certain types (spines and circles) can optionally be faceted. + If filename provided doesn't end with .html then .html will be added. + Viewed from the XZ plane + + Args: + wires (CadQuery.Wire): the wire (edge) or list of wires to plot points + from and to optionally facet. + filename: the filename used to save the html graph. + view_plane: The axis to view the points and faceted edges from. The + options are 'XZ', 'XY', 'YZ', 'YX', 'ZY', 'ZX', 'RZ' and 'XYZ'. + Defaults to 'RZ' + facet_splines: If True then spline edges will be faceted. Defaults to + True. + facet_circles: If True then circle edges will be faceted. Defaults to + True. + tolerance: faceting toleranceto use when faceting cirles and splines. + Defaults to 1e-3. + title: the title of the plotly plot. + + Returns: + plotly.Figure(): figure object + """ + + 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=title, hovermode="closest") + + if view_plane == 'XYZ': + fig.update_layout( + title=title, + scene_aspectmode='data', + scene=dict( + xaxis_title=view_plane[0], + yaxis_title=view_plane[1], + zaxis_title=view_plane[2], + ), + ) + else: + + fig.update_layout( + yaxis=dict(scaleanchor="x", + scaleratio=1), + xaxis_title=view_plane[0], + yaxis_title=view_plane[1] + ) + + if isinstance(wires, list): + list_of_wires = wires + else: + list_of_wires = [wires] + + for counter, wire in enumerate(list_of_wires): + + edges = facet_wire( + wire=wire, + facet_splines=facet_splines, + facet_circles=facet_circles, + tolerance=tolerance + ) + + points = paramak.utils.extract_points_from_edges( + edges=edges, + view_plane=view_plane + ) + + fig.add_trace( + plotly_trace( + points=points, + mode="markers+lines", + name='edge ' + str(counter) + ) + ) + + for counter, wire in enumerate(list_of_wires): + + if isinstance(wire, cq.occ_impl.shapes.Wire): + # this is for imported stp files + edges = wire.Edges() + else: + # this is for cadquery generated solids + edges = wire.val().Edges() + + points = paramak.utils.extract_points_from_edges( + edges=edges, + view_plane=view_plane) + + fig.add_trace(plotly_trace( + points=points, + mode="markers", + name='points on wire ' + str(counter) + ) + ) + + fig.write_html(str(path_filename)) + + print("Exported html graph to ", path_filename) + + return fig + + 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 @@ -336,20 +670,20 @@ def __init__(self, area, tolerance=0.1): self.area = area self.tolerance = tolerance - def filter(self, objectList): + def filter(self, object_list): """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. + object_list (cadquery): The object to filter the faces from. Returns: - objectList (cadquery): The face that match the selector area within + object_list (cadquery): The face that match the selector area within the specified tolerance. """ new_obj_list = [] - for obj in objectList: + for obj in object_list: face_area = obj.Area() # Only return faces that meet the requirements @@ -376,21 +710,21 @@ def __init__(self, length: float, tolerance: float = 0.1): self.length = length self.tolerance = tolerance - def filter(self, objectList): + def filter(self, object_list): """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. + object_list (cadquery): The object to filter the edges from. Returns: - objectList (cadquery): The edge that match the selector length + object_list (cadquery): The edge that match the selector length within the specified tolerance. """ new_obj_list = [] print('filleting edge#') - for obj in objectList: + for obj in object_list: edge_len = obj.Length() diff --git a/run_tests.sh b/run_tests.sh index e600350eb..a351633a2 100644 --- a/run_tests.sh +++ b/run_tests.sh @@ -1,4 +1,7 @@ -pytest tests/test_neutronics_utils.py -v --cov=paramak --cov-append --cov-report term --cov-report xml +#!/bin/bash + +pytest tests/test_neutronics_utils.py -v --cov=paramak --cov-report term --cov-report xml +pytest tests/test_example_neutronics_simulations.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 diff --git a/setup.py b/setup.py index adce924a6..cb75b1556 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="paramak", - version="0.2.2", + version="0.2.3", author="The Paramak Development Team", author_email="jonathan.shimwell@ukaea.uk", description="Create 3D fusion reactor CAD models based on input parameters", diff --git a/tests/test_Reactor.py b/tests/test_Reactor.py index 7701f5f5a..eccb94ef7 100644 --- a/tests/test_Reactor.py +++ b/tests/test_Reactor.py @@ -11,6 +11,12 @@ class TestReactor(unittest.TestCase): + def setUp(self): + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20)]) + + self.test_reactor = paramak.Reactor([test_shape]) + 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.""" @@ -451,13 +457,9 @@ def test_exported_svg_files_exist(self): 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") + self.test_reactor.export_svg("test_svg_image.svg") assert Path("test_svg_image.svg").exists() is True os.system("rm test_svg_image.svg") @@ -467,17 +469,46 @@ def test_exported_svg_files_exist_no_extension(self): 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") + self.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_export_svg_options(self): + """Exports the test reacto to an svg image and checks that a svg file + can be exported with the various different export options""" + + os.system("rm *.svg") + self.test_reactor.export_svg("r_width.svg", width=900) + assert Path("r_width.svg").exists() is True + self.test_reactor.export_svg("r_height.svg", height=900) + assert Path("r_height.svg").exists() is True + self.test_reactor.export_svg("r_marginLeft.svg", marginLeft=110) + assert Path("r_marginLeft.svg").exists() is True + self.test_reactor.export_svg("r_marginTop.svg", marginTop=110) + assert Path("r_marginTop.svg").exists() is True + self.test_reactor.export_svg("r_showAxes.svg", showAxes=True) + assert Path("r_showAxes.svg").exists() is True + self.test_reactor.export_svg( + "r_projectionDir.svg", projectionDir=(-1, -1, -1)) + assert Path("r_projectionDir.svg").exists() is True + self.test_reactor.export_svg( + "r_strokeColor.svg", strokeColor=( + 42, 42, 42)) + assert Path("r_strokeColor.svg").exists() is True + self.test_reactor.export_svg( + "r_hiddenColor.svg", hiddenColor=( + 42, 42, 42)) + assert Path("r_hiddenColor.svg").exists() is True + self.test_reactor.export_svg("r_showHidden.svg", showHidden=False) + assert Path("r_showHidden.svg").exists() is True + self.test_reactor.export_svg("r_strokeWidth1.svg", strokeWidth=None) + assert Path("r_strokeWidth1.svg").exists() is True + self.test_reactor.export_svg("r_strokeWidth2.svg", strokeWidth=10) + assert Path("r_strokeWidth2.svg").exists() is True + def test_neutronics_description(self): """Creates reactor objects to check errors are raised correctly when exporting the neutronics description.""" diff --git a/tests/test_Shape.py b/tests/test_Shape.py index 950f60a48..90dc775ba 100644 --- a/tests/test_Shape.py +++ b/tests/test_Shape.py @@ -140,20 +140,6 @@ 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.""" @@ -257,6 +243,22 @@ def test_export_html(self): assert Path("filename.html").exists() is True os.system("rm filename.html") + def test_export_html_view_planes(self): + """Checks a plotly figure of the Shape is exported by the export_html + method with a range of different view_plane options.""" + + test_shape = paramak.RotateStraightShape( + points=[(0, 0), (0, 20), (20, 20), (20, 0)], rotation_angle=180 + ) + + for view_plane in ['XZ', 'XY', 'YZ', 'YX', 'ZY', 'ZX', 'RZ', 'XYZ']: + os.system("rm *.html") + test_shape.export_html( + filename='filename', + view_plane=view_plane + ) + assert Path("filename.html").exists() is True + def test_export_html_with_points_None(self): """Checks that an error is raised when points is None and export_html """ @@ -266,6 +268,16 @@ def export(): test_shape.export_html("out.html") self.assertRaises(ValueError, export) + def test_export_html_with_wire_None(self): + """Checks that an error is raised when wire is None and export_html + """ + test_shape = paramak.Shape(points=[(0, 0), (0, 20), (20, 20), (20, 0)]) + test_shape.wire = None + + 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.""" @@ -404,17 +416,6 @@ def test_areas_add_up_to_total_area(self): 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.""" diff --git a/tests/test_example_components.py b/tests/test_example_components.py index 4ceaa4ae2..adf4c267a 100644 --- a/tests/test_example_components.py +++ b/tests/test_example_components.py @@ -12,9 +12,6 @@ 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): @@ -61,7 +58,7 @@ def test_make_all_parametric_components(self): filenames.append(components.stp_filename) for output_filename in output_filenames: - assert Path(output_filename).exists() is True + assert Path(output_filename).exists() os.system("rm " + output_filename) def test_make_plasma(self): @@ -85,7 +82,7 @@ def test_make_plasma(self): os.system("rm " + output_filename) make_plasmas.main() for output_filename in output_filenames: - assert Path(output_filename).exists() is True + assert Path(output_filename).exists() os.system("rm " + output_filename) def test_make_demo_style_blanket(self): @@ -93,7 +90,7 @@ def test_make_demo_style_blanket(self): output_filename = "blanket.stp" os.system("rm " + output_filename) make_demo_style_blankets.main() - assert Path(output_filename).exists() is True + assert Path(output_filename).exists() os.system("rm " + output_filename) def test_make_segmented_firstwall(self): @@ -101,7 +98,7 @@ def test_make_segmented_firstwall(self): output_filename = "segmented_firstwall.stp" os.system("rm " + output_filename) make_firstwall_for_neutron_wall_loading.main() - assert Path(output_filename).exists() is True + assert Path(output_filename).exists() os.system("rm " + output_filename) def test_make_vacuum_vessel(self): @@ -114,7 +111,7 @@ def test_make_vacuum_vessel(self): os.system("rm " + output_filename) make_vacuum_vessel_with_ports.main() for output_filename in output_filenames: - assert Path(output_filename).exists() is True + assert Path(output_filename).exists() os.system("rm " + output_filename) diff --git a/tests/test_example_neutronics_simulations.py b/tests/test_example_neutronics_simulations.py new file mode 100644 index 000000000..10a523f72 --- /dev/null +++ b/tests/test_example_neutronics_simulations.py @@ -0,0 +1,40 @@ + +import os +import sys +import unittest +from pathlib import Path + +from examples.example_neutronics_simulations import ( + shape_with_gas_production) + +sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'examples')) + + +class TestExampleNeutronics(unittest.TestCase): + + def test_make_cad_from_points(self): + """Runs the example and checks the output files are produced""" + output_filenames = [ + "results.json", + "n-Xp_on_2D_mesh_yz.png", + "n-Xp_on_2D_mesh_xy.png", + "n-Xp_on_2D_mesh_xz.png", + "n-Xt_on_2D_mesh_yz.png", + "n-Xt_on_2D_mesh_xy.png", + "n-Xt_on_2D_mesh_xz.png", + "n-Xa_on_2D_mesh_yz.png", + "n-Xa_on_2D_mesh_xy.png", + "n-Xa_on_2D_mesh_xz.png", + "n-Xp_on_3D_mesh.vtk", + "n-Xt_on_3D_mesh.vtk", + "n-Xa_on_3D_mesh.vtk", + ] + os.system("rm *.png") + os.system("rm *.vtk") + shape_with_gas_production.main() + for output_filename in output_filenames: + assert Path(output_filename).exists() is True + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_example_reactors.py b/tests/test_example_reactors.py index 5ee47f4a9..0f9e66b6a 100644 --- a/tests/test_example_reactors.py +++ b/tests/test_example_reactors.py @@ -5,13 +5,28 @@ from pathlib import Path from examples.example_parametric_reactors import ( - ball_reactor, ball_reactor_single_null, submersion_reactor_single_null, - htc_reactor) + ball_reactor, ball_reactor_single_null, htc_reactor, make_animation, + submersion_reactor_single_null) sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'examples')) class TestExampleReactors(unittest.TestCase): + + def test_make_animations(self): + """Runs the example to check the output files are produced""" + output_filenames = [ + "random_0000.svg", + "random_0001.svg", + "rotation_0000.svg", + "rotation_0001.svg", + ] + os.system("rm *.svg") + make_animation.rotate_single_reactor(2) + make_animation.make_random_reactors(2) + for output_filename in output_filenames: + assert Path(output_filename).exists() is True + def test_make_parametric_htc_rector(self): """Runs the example to check the output files are produced""" output_filenames = [ diff --git a/tests/test_example_shapes.py b/tests/test_example_shapes.py index 8a47b6216..1c27e4387 100644 --- a/tests/test_example_shapes.py +++ b/tests/test_example_shapes.py @@ -7,7 +7,10 @@ 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) + make_can_reactor_from_points, make_html_diagram_from_stp_file, +) + +import paramak sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'examples')) @@ -17,18 +20,18 @@ 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) + os.system("rm *.stp") make_blanket_from_points.main(filename=filename) assert Path(filename).exists() is True - os.system("rm " + filename) + os.system("rm *.stp") 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) + os.system("rm *.stp") make_blanket_from_parameters.main(filename=filename) assert Path(filename).exists() is True - os.system("rm " + filename) + os.system("rm *.stp") def test_make_cad_from_points(self): """Runs the example and checks the output files are produced""" @@ -40,12 +43,10 @@ def test_make_cad_from_points(self): "rotated_spline.stp", "rotated_straights.stp", ] - for output_filename in output_filenames: - os.system("rm " + output_filename) + os.system("rm *.stp") 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""" @@ -59,12 +60,11 @@ def test_make_can_reactor_from_parameters(self): "can_reactor_from_parameters/core.stp", "can_reactor_from_parameters/reactor.html", ] - for output_filename in output_filenames: - os.system("rm " + output_filename) + os.system("rm *.stp") + os.system("rm *.html") 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""" @@ -78,12 +78,84 @@ def test_make_can_reactor_from_points(self): "can_reactor_from_points/core.stp", "can_reactor_from_points/reactor.html", ] - for output_filename in output_filenames: - os.system("rm " + output_filename) + os.system("rm *.stp") + os.system("rm *.html") make_can_reactor_from_points.main() for output_filename in output_filenames: assert Path(output_filename).exists() is True - os.system("rm " + output_filename) + + def test_make_html_diagram_from_stp_file(self): + """Runs the example and checks the output files are produced""" + output_filenames = [ + "example_shape.stp", + "example_shape_RZ.html", + "example_shape_XYZ.html", + "example_shape_XZ.html", + "example_shape_from_stp_RZ.html", + "example_shape_from_stp_XZ.html", + "example_shape_from_stp_XYZ.html", + ] + os.system("rm *.stp") + os.system("rm *.html") + make_html_diagram_from_stp_file.make_stp_file() + make_html_diagram_from_stp_file.load_stp_file_and_plot() + for output_filename in output_filenames: + assert Path(output_filename).exists() is True + + def test_list_of_wires_can_be_exported(self): + """Checks than a list of wires is an acceptable input + for export_wire_to_html wires argument. + """ + + example_shape = paramak.ExtrudeMixedShape( + distance=1, + points=[ + (150, 100, "spline"), + (140, 75, "spline"), + (110, 45, "spline"), + ] + ) + + fig = paramak.utils.export_wire_to_html( + wires=[example_shape.wire], + tolerance=0.1, + view_plane="XY", + facet_splines=True, + facet_circles=True, + filename="example_shape_from_stp.html", + ) + + assert fig is not None + + def test_incorrect_view_plane(self): + """Checks than an error is raised when incorrect values of the + view_plane is set + """ + + def set_value(): + example_shape = paramak.ExtrudeMixedShape( + distance=1, + points=[ + (100, 0, "straight"), + (200, 0, "circle"), + (250, 50, "circle"), + (200, 100, "straight"), + (150, 100, "spline"), + (140, 75, "spline"), + (110, 45, "spline"), + ] + ) + + paramak.utils.export_wire_to_html( + wires=example_shape.wire, + tolerance=0.1, + view_plane="coucou", + facet_splines=True, + facet_circles=True, + filename="example_shape_from_stp.html", + ) + + self.assertRaises(ValueError, set_value) if __name__ == "__main__": diff --git a/tests/test_neutronics_utils.py b/tests/test_neutronics_utils.py index 5d3055c5d..bd36f6dfe 100644 --- a/tests/test_neutronics_utils.py +++ b/tests/test_neutronics_utils.py @@ -38,5 +38,5 @@ def test_moab_instance_creation(self): new_moab_core.write_file('test_file.h5m') - assert Path('test_file.stl').exists() is True - assert Path('test_file.h5m').exists() is True + assert Path('test_file.stl').exists() + assert Path('test_file.h5m').exists() diff --git a/tests/test_parametric_components/test_BlanketCutterParallels.py b/tests/test_parametric_components/test_BlanketCutterParallels.py index ca4233902..5d3733167 100644 --- a/tests/test_parametric_components/test_BlanketCutterParallels.py +++ b/tests/test_parametric_components/test_BlanketCutterParallels.py @@ -47,7 +47,6 @@ def test_cut_modification(self): 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( diff --git a/tests/test_parametric_components/test_HexagonPin.py b/tests/test_parametric_components/test_HexagonPin.py new file mode 100644 index 000000000..59d1bed40 --- /dev/null +++ b/tests/test_parametric_components/test_HexagonPin.py @@ -0,0 +1,73 @@ + +import math +import unittest + +import paramak +import pytest + + +class TestHexagonPin(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.HexagonPin( + length_of_side=5, distance=42., center_point=(0, 0)) + + def test_setting_parameters(self): + """Checks that the default parameters and user parameters are set""" + + assert self.test_shape.length_of_side == 5 + assert self.test_shape.distance == 42. + assert self.test_shape.center_point == (0, 0) + assert self.test_shape.stp_filename == "HexagonPin.stp" + assert self.test_shape.stl_filename == "HexagonPin.stl" + assert self.test_shape.name == "hexagon_pin" + assert self.test_shape.material_tag == "hexagon_pin_mat" + + def test_volume(self): + """Checks the volume against the actual value""" + + length = self.test_shape.length_of_side + distance = self.test_shape.distance + + hexagon_face_area = (3 * math.sqrt(3) / 2) * math.pow(length, 2) + # this needs a pytest.approx() as the volumes are not exact + assert pytest.approx(self.test_shape.volume, + rel=0.1) == hexagon_face_area * distance + + def test_distance_impacts_volume(self): + """Checks that changing the distance argument results in the + expected volume change""" + + test_shape_volume = self.test_shape.volume + + self.test_shape.distance = self.test_shape.distance * 2 + + assert pytest.approx(test_shape_volume * 2, + rel=0.1) == self.test_shape.volume + + def test_length_of_sides_impacts_volume(self): + """Checks that changing the length_of_sides argument results in a the + expected volume change""" + + test_shape_volume = self.test_shape.volume + + self.test_shape.length_of_side = self.test_shape.length_of_side * 2 + + assert pytest.approx(test_shape_volume * 4, + rel=0.1) == self.test_shape.volume + + def test_areas_are_correct(self): + """Tests the areas of the faces are the correct sizes""" + + test_shape_areas = self.test_shape.areas + + length = self.test_shape.length_of_side + distance = self.test_shape.distance + + hexagon_face_area = (3 * math.sqrt(3) / 2) * math.pow(length, 2) + + assert len(test_shape_areas) == 8 + assert test_shape_areas.count(pytest.approx(hexagon_face_area, + rel=0.1)) == 2 + assert test_shape_areas.count(pytest.approx(length * distance, + rel=0.1)) == 6 diff --git a/tests/test_parametric_components/test_PortCutterRotated.py b/tests/test_parametric_components/test_PortCutterRotated.py index 9c51912e2..1040289d9 100644 --- a/tests/test_parametric_components/test_PortCutterRotated.py +++ b/tests/test_parametric_components/test_PortCutterRotated.py @@ -151,7 +151,7 @@ def test_outerpoint_negative(self): """Tests that when polar_coverage_angle is greater than 180 an error is raised.""" def error(): - shape = paramak.PortCutterRotated( + paramak.PortCutterRotated( center_point=(1, 1), polar_coverage_angle=181, polar_placement_angle=0, diff --git a/tests/test_parametric_neutronics/test_NeutronicModel.py b/tests/test_parametric_neutronics/test_NeutronicModel.py index d3ef8f780..9bf9c05c3 100644 --- a/tests/test_parametric_neutronics/test_NeutronicModel.py +++ b/tests/test_parametric_neutronics/test_NeutronicModel.py @@ -490,7 +490,7 @@ def test_neutronics_component_3d_mesh_simulation(self): geometry=self.my_shape, source=self.source, materials={'center_column_shield_mat': 'Be'}, - mesh_tally_3d=['heating', 'tritium_production'], + mesh_tally_3d=['heating', '(n,Xt)'], simulation_batches=2, simulation_particles_per_batch=2 ) @@ -504,7 +504,7 @@ def test_neutronics_component_3d_mesh_simulation(self): assert Path(output_filename).exists() is True assert Path('heating_on_3D_mesh.vtk').exists() is True - assert Path('tritium_production_on_3D_mesh.vtk').exists() is True + assert Path('n-Xt_on_3D_mesh.vtk').exists() is True def test_batches_and_particles_convert_to_int(self): """Makes a neutronics model and simulates with a 3D and 2D mesh tally @@ -690,7 +690,7 @@ def test_reactor_from_shapes_2d_mesh_tallies(self): 'mat1': 'copper', 'blanket_mat': 'FLiNaK', # used as O18 is not in nndc nuc data }, - mesh_tally_2d=['tritium_production', 'heating', 'flux'], + mesh_tally_2d=['(n,Xt)', 'heating', 'flux'], simulation_batches=2, simulation_particles_per_batch=10, ) @@ -698,9 +698,9 @@ def test_reactor_from_shapes_2d_mesh_tallies(self): # starts the neutronics simulation using trelis neutronics_model.simulate(verbose=False, method='pymoab') - 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("n-Xt_on_2D_mesh_xz.png").exists() is True + assert Path("n-Xt_on_2D_mesh_xy.png").exists() is True + assert Path("n-Xt_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 diff --git a/tests/test_parametric_neutronics/test_Shape_neutronics.py b/tests/test_parametric_neutronics/test_Shape_neutronics.py index 7bc439065..03006c896 100644 --- a/tests/test_parametric_neutronics/test_Shape_neutronics.py +++ b/tests/test_parametric_neutronics/test_Shape_neutronics.py @@ -245,7 +245,7 @@ def test_cylinder_cask(self): cad_result = self.simulate_cylinder_cask_cad( test_material, source, height, outer_radius, thickness, batches, particles) - assert pytest.approx(csg_result, rel=0.001) == cad_result + assert pytest.approx(csg_result, rel=0.02) == cad_result if __name__ == "__main__": diff --git a/tests/test_parametric_reactors/test_BallReactor.py b/tests/test_parametric_reactors/test_BallReactor.py index 181d30911..74744fd8f 100644 --- a/tests/test_parametric_reactors/test_BallReactor.py +++ b/tests/test_parametric_reactors/test_BallReactor.py @@ -37,7 +37,7 @@ def test_creation_with_narrow_divertor(self): assert self.test_reactor.solid is not None assert len(self.test_reactor.shapes_and_components) == 7 - def test_creation_with_narrow_divertor(self): + def test_creation_with_wide_divertor(self): """Creates a BallReactor with a wide divertor and checks that the correct number of components are created.""" diff --git a/tests/test_parametric_shapes/test_ExtrudeSplineShape.py b/tests/test_parametric_shapes/test_ExtrudeSplineShape.py index b77f6765f..c04685620 100644 --- a/tests/test_parametric_shapes/test_ExtrudeSplineShape.py +++ b/tests/test_parametric_shapes/test_ExtrudeSplineShape.py @@ -107,6 +107,23 @@ def test_export_stp(self): os.system("rm test_solid.stp test_solid2.stp test_wire.stp") + def test_incorrect_points_input(self): + """Checks that an error is raised when the points are input with the + connection""" + + def incorrect_points_definition(): + self.test_shape.points = [ + (10, 10, 'spline'), + (10, 30, 'spline'), + (30, 30, 'spline'), + (30, 10, 'spline') + ] + + self.assertRaises( + ValueError, + incorrect_points_definition + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_parametric_shapes/test_ExtrudeStraightShape.py b/tests/test_parametric_shapes/test_ExtrudeStraightShape.py index ee7be22bc..921ae3632 100644 --- a/tests/test_parametric_shapes/test_ExtrudeStraightShape.py +++ b/tests/test_parametric_shapes/test_ExtrudeStraightShape.py @@ -121,6 +121,35 @@ def test_export_stp(self): os.system("rm test_solid.stp test_solid2.stp test_wire.stp") + def test_incorrect_points_input(self): + """Checks that an error is raised when the points are input with the + connection""" + + def incorrect_points_definition(): + self.test_shape.points = [ + (10, 10, 'straight'), + (10, 30, 'straight'), + (30, 30, 'straight'), + (30, 10, 'straight') + ] + + self.assertRaises( + ValueError, + incorrect_points_definition + ) + + def test_export_html_with_different_workplanes(self): + """Checks that all the workplanes produce an html file when using the + export_html method and that the axis have the correct labels""" + + os.system("rm *.html") + for workplane in ["XY", "YZ", "XZ", "YX", "ZY", "ZX"]: + self.test_shape.workplane = workplane + fig = self.test_shape.export_html(workplane + ".html") + assert Path(workplane + ".html").exists() is True + assert fig.layout.xaxis.title['text'] == workplane[0] + assert fig.layout.yaxis.title['text'] == workplane[1] + if __name__ == "__main__": unittest.main() diff --git a/tests/test_parametric_shapes/test_RotateMixedShape.py b/tests/test_parametric_shapes/test_RotateMixedShape.py index 48493e211..61d66aace 100644 --- a/tests/test_parametric_shapes/test_RotateMixedShape.py +++ b/tests/test_parametric_shapes/test_RotateMixedShape.py @@ -15,6 +15,19 @@ def setUp(self): (70, 50, "circle"), (60, 25, "circle"), (70, 0, "straight")] ) + def test_export_2d_image(self): + """Creates a RotateMixedShape object and checks that a png file of the + object with the correct suffix can be exported using the + export_2d_image method.""" + + os.system("rm filename.png") + self.test_shape.export_2d_image("filename") + assert Path("filename.png").exists() is True + os.system("rm filename.png") + self.test_shape.export_2d_image("filename.png") + assert Path("filename.png").exists() is True + os.system("rm filename.png") + def test_default_parameters(self): """Checks that the default parameters of a RotateMixedShape are correct.""" diff --git a/tests/test_parametric_shapes/test_RotateSplineShape.py b/tests/test_parametric_shapes/test_RotateSplineShape.py index 63d68ce83..fe69b1aff 100644 --- a/tests/test_parametric_shapes/test_RotateSplineShape.py +++ b/tests/test_parametric_shapes/test_RotateSplineShape.py @@ -90,6 +90,23 @@ def test_export_stp(self): os.system("rm test_solid.stp test_solid2.stp test_wire.stp") + def test_incorrect_points_input(self): + """Checks that an error is raised when the points are input with the + connection""" + + def incorrect_points_definition(): + self.test_shape.points = [ + (10, 10, 'spline'), + (10, 30, 'spline'), + (30, 30, 'spline'), + (30, 10, 'spline') + ] + + self.assertRaises( + ValueError, + incorrect_points_definition + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_parametric_shapes/test_RotateStraightShape.py b/tests/test_parametric_shapes/test_RotateStraightShape.py index 5c3050280..5dc97c818 100644 --- a/tests/test_parametric_shapes/test_RotateStraightShape.py +++ b/tests/test_parametric_shapes/test_RotateStraightShape.py @@ -180,6 +180,35 @@ def test_export_svg(self): assert Path("filename.svg").exists() is True os.system("rm filename.svg") + def test_export_svg_options(self): + """Creates a RotateStraightShape and checks that a svg file of the + shape can be exported with the various different export options""" + + os.system("rm *.svg") + self.test_shape.export_svg("width.svg", width=900) + assert Path("width.svg").exists() is True + self.test_shape.export_svg("height.svg", height=900) + assert Path("height.svg").exists() is True + self.test_shape.export_svg("marginLeft.svg", marginLeft=110) + assert Path("marginLeft.svg").exists() is True + self.test_shape.export_svg("marginTop.svg", marginTop=110) + assert Path("marginTop.svg").exists() is True + self.test_shape.export_svg("showAxes.svg", showAxes=True) + assert Path("showAxes.svg").exists() is True + self.test_shape.export_svg( + "projectionDir.svg", projectionDir=(-1, -1, -1)) + assert Path("projectionDir.svg").exists() is True + self.test_shape.export_svg("strokeColor.svg", strokeColor=(42, 42, 42)) + assert Path("strokeColor.svg").exists() is True + self.test_shape.export_svg("hiddenColor.svg", hiddenColor=(42, 42, 42)) + assert Path("hiddenColor.svg").exists() is True + self.test_shape.export_svg("showHidden.svg", showHidden=False) + assert Path("showHidden.svg").exists() is True + self.test_shape.export_svg("strokeWidth1.svg", strokeWidth=None) + assert Path("strokeWidth1.svg").exists() is True + self.test_shape.export_svg("strokeWidth2.svg", strokeWidth=10) + assert Path("strokeWidth2.svg").exists() is True + def test_cut_volume(self): """Creates a RotateStraightShape with another RotateStraightShape cut out and checks that the volume is correct.""" @@ -285,6 +314,23 @@ def test_graveyard_filename(self): output_filename = self.test_shape.export_graveyard(filename='test2') assert 'test2.stp' == output_filename + def test_incorrect_points_input(self): + """Checks that an error is raised when the points are input with the + connection""" + + def incorrect_points_definition(): + self.test_shape.points = [ + (10, 10, 'straight'), + (10, 30, 'straight'), + (30, 30, 'straight'), + (30, 10, 'straight') + ] + + self.assertRaises( + ValueError, + incorrect_points_definition + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_parametric_shapes/test_SweepCircleShape.py b/tests/test_parametric_shapes/test_SweepCircleShape.py index d5b97b37c..174c92dee 100644 --- a/tests/test_parametric_shapes/test_SweepCircleShape.py +++ b/tests/test_parametric_shapes/test_SweepCircleShape.py @@ -25,7 +25,7 @@ def test_default_parameters(self): 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 + assert self.test_shape.force_cross_section is False def test_solid_construction_workplane(self): """Checks that SweepSplineShapes can be created in different workplanes.""" diff --git a/tests/test_parametric_shapes/test_SweepSplineShape.py b/tests/test_parametric_shapes/test_SweepSplineShape.py index 121994c26..d93155ca5 100644 --- a/tests/test_parametric_shapes/test_SweepSplineShape.py +++ b/tests/test_parametric_shapes/test_SweepSplineShape.py @@ -117,6 +117,23 @@ def test_export_stp(self): os.system("rm test_solid.stp test_solid2.stp test_wire.stp") + def test_incorrect_points_input(self): + """Checks that an error is raised when the points are input with the + connection""" + + def incorrect_points_definition(): + self.test_shape.points = [ + (10, 10, 'spline'), + (10, 30, 'spline'), + (30, 30, 'spline'), + (30, 10, 'spline') + ] + + self.assertRaises( + ValueError, + incorrect_points_definition + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_parametric_shapes/test_SweepStraightShape.py b/tests/test_parametric_shapes/test_SweepStraightShape.py index b36d11385..7eb968998 100644 --- a/tests/test_parametric_shapes/test_SweepStraightShape.py +++ b/tests/test_parametric_shapes/test_SweepStraightShape.py @@ -105,6 +105,23 @@ def test_export_stp(self): os.system("rm test_solid.stp test_solid2.stp test_wire.stp") + def test_incorrect_points_input(self): + """Checks that an error is raised when the points are input with the + connection""" + + def incorrect_points_definition(): + self.test_shape.points = [ + (10, 10, 'straight'), + (10, 30, 'straight'), + (30, 30, 'straight'), + (30, 10, 'straight') + ] + + self.assertRaises( + ValueError, + incorrect_points_definition + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_utils.py b/tests/test_utils.py index bf1564f85..9db12a3ba 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,14 +1,83 @@ import unittest +from cadquery.cq import Workplane import numpy as np import paramak from paramak.utils import (EdgeLengthSelector, FaceAreaSelector, - find_center_point_of_circle) + find_center_point_of_circle, plotly_trace, + extract_points_from_edges, facet_wire) +import plotly.graph_objects as go class TestUtilityFunctions(unittest.TestCase): + def test_extract_points_from_edges(self): + """Extracts points from edges and checks the list returned is the + correct len and contains the correct types""" + + test_points = [(1, 1), (3, 1), (4, 2)] + test_shape = paramak.ExtrudeStraightShape( + points=test_points, + distance=6, + workplane='YZ') + + edges = facet_wire(wire=test_shape.wire) + + points = extract_points_from_edges(edges=edges, view_plane='YZ') + + assert len(points) == 6 + + for point in points: + assert len(point) == 2 + assert isinstance(point[0], float) + assert isinstance(point[1], float) + + points_single_edge = extract_points_from_edges(edges=edges[0], view_plane='YZ') + + assert len(points) > len(points_single_edge) + for point in points_single_edge: + assert len(point) == 2 + assert isinstance(point[0], float) + assert isinstance(point[1], float) + + points_single_edge = extract_points_from_edges(edges=edges[0], view_plane='XYZ') + + assert len(points) > len(points_single_edge) + for point in points_single_edge: + assert len(point) == 3 + assert isinstance(point[0], float) + assert isinstance(point[1], float) + assert isinstance(point[2], float) + + def test_trace_creation(self): + """Creates a plotly trace and checks the type returned""" + trace = plotly_trace( + points=[ + (0, 20), + (20, 0), + (0, -20) + ], + mode='markers+lines', + color=(10, 10, 10, 0.5) + ) + + assert isinstance(trace, go.Scatter) + + def test_trace_creation_3d(self): + """Creates a 3d plotly trace and checks the type returned""" + trace = plotly_trace( + points=[ + (0, 20, 0), + (20, 0, 10), + (0, -20, -10) + ], + mode='markers+lines', + color=(10, 10, 10) + ) + + assert isinstance(trace, go.Scatter3d) + 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"""