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) - + "