From 4b4d4a1244e9dd6b533d3299bc47823a87684b4d Mon Sep 17 00:00:00 2001 From: janvonrickenbach Date: Mon, 19 Feb 2024 12:01:24 +0100 Subject: [PATCH] Sandwich and selection rule examples (#423) * Add new examples for a sandwich panel, basic and advanced selection rules. * Small improvement to the Virtual Geometries API * Add tests for the create method with non-default arguments for all the objects. --- examples/003_sandwich_panel.py | 181 +++++++++++ examples/004_basic_rules.py | 156 +++++++++ examples/005_advanced_rules.py | 299 ++++++++++++++++++ .../_tree_objects/linked_selection_rule.py | 5 +- .../_tree_objects/oriented_selection_set.py | 18 ++ .../core/_tree_objects/virtual_geometry.py | 27 +- src/ansys/acp/core/example_helpers.py | 71 +++++ tests/unittests/common/tree_object_tester.py | 44 ++- .../unittests/test_boolean_selection_rule.py | 14 +- tests/unittests/test_cad_geometry.py | 22 +- tests/unittests/test_cutoff_selection_rule.py | 24 +- .../test_cylindrical_selection_rule.py | 24 +- tests/unittests/test_edge_set.py | 20 +- tests/unittests/test_element_set.py | 17 +- tests/unittests/test_fabric.py | 26 +- .../test_geometrical_selection_rule.py | 26 +- tests/unittests/test_lookup_table_1d.py | 14 +- tests/unittests/test_lookup_table_3d.py | 18 +- tests/unittests/test_lookup_table_column.py | 46 ++- tests/unittests/test_material.py | 11 +- tests/unittests/test_modeling_group.py | 7 +- tests/unittests/test_modeling_ply.py | 49 +-- .../unittests/test_oriented_selection_set.py | 39 ++- .../unittests/test_parallel_selection_rule.py | 26 +- tests/unittests/test_rosette.py | 19 +- tests/unittests/test_sensor.py | 27 +- .../test_spherical_selection_rule.py | 22 +- tests/unittests/test_stackup.py | 30 +- tests/unittests/test_sublaminate.py | 16 +- tests/unittests/test_tube_selection_rule.py | 28 +- .../test_variable_offset_selection_rule.py | 30 +- tests/unittests/test_virtual_geometry.py | 46 ++- 32 files changed, 1167 insertions(+), 235 deletions(-) create mode 100644 examples/003_sandwich_panel.py create mode 100644 examples/004_basic_rules.py create mode 100644 examples/005_advanced_rules.py diff --git a/examples/003_sandwich_panel.py b/examples/003_sandwich_panel.py new file mode 100644 index 0000000000..91a417776f --- /dev/null +++ b/examples/003_sandwich_panel.py @@ -0,0 +1,181 @@ +""" +.. _basic_sandwich_panel: + +Basic sandwich panel +==================== + +Define a Composite Lay-up for a sandwich panel with PyACP. This example shows just the +pyACP part of the setup. For a complete Composite analysis, +see the :ref:`sphx_glr_examples_gallery_examples_001_basic_flat_plate.py` example +""" + + +# %% +# Import standard library and third-party dependencies +import pathlib +import tempfile + +# %% +# Import pyACP dependencies +from ansys.acp.core import ( + ACPWorkflow, + FabricWithAngle, + Lamina, + PlyType, + get_directions_plotter, + launch_acp, + print_model, +) +from ansys.acp.core.example_helpers import ExampleKeys, get_example_file +from ansys.acp.core.material_property_sets import ConstantEngineeringConstants, ConstantStrainLimits + +# %% +# Get example file from server +tempdir = tempfile.TemporaryDirectory() +WORKING_DIR = pathlib.Path(tempdir.name) +input_file = get_example_file(ExampleKeys.BASIC_FLAT_PLATE_CDB, WORKING_DIR) + +# %% +# Launch the PyACP server and connect to it. +acp = launch_acp() + +# %% +# Define the input file and instantiate an ACPWorkflow +# The ACPWorkflow class provides convenience methods which simplify the file handling. +# It automatically creates a model based on the input file. + +workflow = ACPWorkflow.from_cdb_file( + acp=acp, + cdb_file_path=input_file, + local_working_directory=WORKING_DIR, +) + +model = workflow.model +print(workflow.working_directory.path) +print(model.unit_system) + +# %% +# Visualize the loaded mesh +mesh = model.mesh.to_pyvista() +mesh.plot(show_edges=True) + + +# %% +# Create the UD material and its corresponding fabric +engineering_constants_ud = ConstantEngineeringConstants.from_orthotropic_constants( + E1=5e10, E2=1e10, E3=1e10, nu12=0.28, nu13=0.28, nu23=0.3, G12=5e9, G23=4e9, G31=4e9 +) + +strain_limit = 0.01 +strain_limits = ConstantStrainLimits.from_orthotropic_constants( + eXc=-strain_limit, + eYc=-strain_limit, + eZc=-strain_limit, + eXt=strain_limit, + eYt=strain_limit, + eZt=strain_limit, + eSxy=strain_limit, + eSyz=strain_limit, + eSxz=strain_limit, +) + +ud_material = model.create_material( + name="UD", + ply_type=PlyType.REGULAR, + engineering_constants=engineering_constants_ud, + strain_limits=strain_limits, +) + +ud_fabric = model.create_fabric(name="UD", material=ud_material, thickness=0.002) + +# %% +# Create a multi-axial Stackup and a Sublaminate. Sublaminates and Stackups help to quickly +# build repeating laminates. + +biax_carbon_ud = model.create_stackup( + name="Biax_Carbon_UD", + fabrics=( + FabricWithAngle(ud_fabric, -45), + FabricWithAngle(ud_fabric, 45), + ), +) + + +sublaminate = model.create_sublaminate( + name="Sublaminate", + materials=( + Lamina(biax_carbon_ud, 0), + Lamina(ud_fabric, 90), + Lamina(biax_carbon_ud, 0), + ), +) + + +# %% +# Create the Core Material and its corresponding Fabric +engineering_constants_core = ConstantEngineeringConstants.from_isotropic_constants(E=8.5e7, nu=0.3) + +core = model.create_material( + name="Core", + ply_type=PlyType.ISOTROPIC_HOMOGENEOUS_CORE, + engineering_constants=engineering_constants_core, + strain_limits=strain_limits, +) + +core_fabric = model.create_fabric(name="core", material=ud_material, thickness=0.015) + +# %% +# Define a rosette and an oriented selection set and plot the orientations +rosette = model.create_rosette(origin=(0.0, 0.0, 0.0), dir1=(1.0, 0.0, 0.0), dir2=(0.0, 1.0, 0.0)) + +oss = model.create_oriented_selection_set( + name="oss", + orientation_point=(0.0, 0.0, 0.0), + orientation_direction=(0.0, 1.0, 0), + element_sets=[model.element_sets["All_Elements"]], + rosettes=[rosette], +) + +model.update() +assert oss.elemental_data.orientation is not None +plotter = get_directions_plotter(model=model, components=[oss.elemental_data.orientation]) +plotter.show() + +# %% +# Create the modeling plies which define the layup of the sandwich panel. +modeling_group = model.create_modeling_group(name="modeling_group") + +bottom_ply = modeling_group.create_modeling_ply( + name="bottom_ply", + ply_angle=0, + ply_material=sublaminate, + oriented_selection_sets=[oss], +) + +core_ply = modeling_group.create_modeling_ply( + name="core_ply", + ply_angle=0, + ply_material=core_fabric, + oriented_selection_sets=[oss], +) + + +top_ply = modeling_group.create_modeling_ply( + name="top_ply", + ply_angle=90, + ply_material=ud_fabric, + oriented_selection_sets=[oss], + number_of_layers=3, +) + +# %% +# Update and print the model. +model.update() +print_model(workflow.model) +# sphinx_gallery_start_ignore +from ansys.acp.core.example_helpers import _run_analysis + +# Run the analysis so we are sure all the material properties have been correctly +# defined. +_run_analysis(workflow) +# sphinx_gallery_end_ignore diff --git a/examples/004_basic_rules.py b/examples/004_basic_rules.py new file mode 100644 index 0000000000..bae32ac5ac --- /dev/null +++ b/examples/004_basic_rules.py @@ -0,0 +1,156 @@ +""" +.. _basic_rules_example: + +Basic rule example +================== + +Shows the basic usage of selection rules. +Selection Rules enable you to select elements through +geometrical operations and thus to shape plies. +This example shows just the +pyACP part of the setup. See the :ref:`sphx_glr_examples_gallery_examples_005_advanced_rules.py` +for more advanced rule examples. For a complete Composite analysis, +see the :ref:`sphx_glr_examples_gallery_examples_001_basic_flat_plate.py` example +""" + + +# %% +# Import standard library and third-party dependencies +import pathlib +import tempfile + +# %% +# Import pyACP dependencies +from ansys.acp.core import ACPWorkflow, LinkedSelectionRule, PlyType, launch_acp +from ansys.acp.core.example_helpers import ExampleKeys, get_example_file +from ansys.acp.core.material_property_sets import ConstantEngineeringConstants + +# %% +# Get example file from server +tempdir = tempfile.TemporaryDirectory() +WORKING_DIR = pathlib.Path(tempdir.name) +input_file = get_example_file(ExampleKeys.BASIC_FLAT_PLATE_CDB, WORKING_DIR) + +# %% +# Launch the PyACP server and connect to it. +acp = launch_acp() + +# %% +# Define the input file and instantiate an ACPWorkflow +# The ACPWorkflow class provides convenience methods which simplify the file handling. +# It automatically creates a model based on the input file. + +workflow = ACPWorkflow.from_cdb_file( + acp=acp, + cdb_file_path=input_file, + local_working_directory=WORKING_DIR, +) + +model = workflow.model +print(workflow.working_directory.path) +print(model.unit_system) + +# %% +# Visualize the loaded mesh +mesh = model.mesh.to_pyvista() +mesh.plot(show_edges=True) + + +# %% +# Create the UD material and its corresponding fabric +engineering_constants_ud = ConstantEngineeringConstants.from_orthotropic_constants( + E1=5e10, E2=1e10, E3=1e10, nu12=0.28, nu13=0.28, nu23=0.3, G12=5e9, G23=4e9, G31=4e9 +) + +ud_material = model.create_material( + name="UD", + ply_type=PlyType.REGULAR, + engineering_constants=engineering_constants_ud, +) + +ud_fabric = model.create_fabric(name="UD", material=ud_material, thickness=0.002) + +# %% +# Define a rosette and an oriented selection set +rosette = model.create_rosette(origin=(0.0, 0.0, 0.0), dir1=(1.0, 0.0, 0.0), dir2=(0.0, 1.0, 0.0)) + +oss = model.create_oriented_selection_set( + name="oss", + orientation_point=(0.0, 0.0, 0.0), + orientation_direction=(0.0, 1.0, 0), + element_sets=[model.element_sets["All_Elements"]], + rosettes=[rosette], +) + +# %% +# Create a ply with an attached parallel selection rule and plot the ply extent + +parallel_rule = model.create_parallel_selection_rule( + name="parallel_rule", + origin=(0, 0, 0), + direction=(1, 0, 0), + lower_limit=0.005, + upper_limit=1, +) + +modeling_group = model.create_modeling_group(name="modeling_group") + +partial_ply = modeling_group.create_modeling_ply( + name="partial_ply", + ply_angle=90, + ply_material=ud_fabric, + oriented_selection_sets=[oss], + selection_rules=[LinkedSelectionRule(parallel_rule)], + number_of_layers=10, +) + +model.update() +assert model.elemental_data.thickness is not None +model.elemental_data.thickness.get_pyvista_mesh(mesh=model.mesh).plot(show_edges=True) + +# %% +# Create a cylindrical selection rule and add it to the ply. This will intersect the two rules. +cylindrical_rule = model.create_cylindrical_selection_rule( + name="cylindrical_rule", + origin=(0.005, 0, 0.005), + direction=(0, 1, 0), + radius=0.002, +) + +partial_ply.selection_rules.append(LinkedSelectionRule(cylindrical_rule)) + +model.update() +assert model.elemental_data.thickness is not None +model.elemental_data.thickness.get_pyvista_mesh(mesh=model.mesh).plot(show_edges=True) + +# %% +# Create a spherical selection rule and assign it to the ply. Now only the spherical rule is +# active. +spherical_rule = model.create_spherical_selection_rule( + name="spherical_rule", + origin=(0.003, 0, 0.005), + radius=0.002, +) + +partial_ply.selection_rules = [LinkedSelectionRule(spherical_rule)] + +model.update() +assert model.elemental_data.thickness is not None +model.elemental_data.thickness.get_pyvista_mesh(mesh=model.mesh).plot(show_edges=True) + +# %% +# Create a tube selection rule and assign it to the ply. Now only the tube rule is +# active. +tube_rule = model.create_tube_selection_rule( + name="spherical_rule", + # Select the pre-exsting _FIXEDSU edge which is the edge at x=0 + edge_set=model.edge_sets["_FIXEDSU"], + inner_radius=0.001, + outer_radius=0.003, +) + +partial_ply.selection_rules = [LinkedSelectionRule(tube_rule)] + +model.update() +assert model.elemental_data.thickness is not None +model.elemental_data.thickness.get_pyvista_mesh(mesh=model.mesh).plot(show_edges=True) diff --git a/examples/005_advanced_rules.py b/examples/005_advanced_rules.py new file mode 100644 index 0000000000..ed22c8afca --- /dev/null +++ b/examples/005_advanced_rules.py @@ -0,0 +1,299 @@ +""" +.. _advanced_rules_example: + +Advanced rules example +====================== + +This example shows the usage of advanced rules such as geometrical rule, +cut-off rule and variable offset rule. It also demonstrates how rules can be templated +and reused with different parameters. +See the :ref:`sphx_glr_examples_gallery_examples_004_basic_rules.py` for more basic rule examples. +This example shows just the pyACP part of the setup. For a complete Composite analysis, +see the :ref:`sphx_glr_examples_gallery_examples_001_basic_flat_plate.py` example +""" + + +# %% +# Import standard library and third-party dependencies +import pathlib +import tempfile + +import numpy as np +import pyvista + +# %% +# Import pyACP dependencies +from ansys.acp.core import ( + ACPWorkflow, + BooleanOperationType, + DimensionType, + EdgeSetType, + LinkedSelectionRule, + PlyType, + example_helpers, + launch_acp, +) +from ansys.acp.core.example_helpers import ExampleKeys, get_example_file +from ansys.acp.core.material_property_sets import ConstantEngineeringConstants + +# %% +# Get example file from server +tempdir = tempfile.TemporaryDirectory() +WORKING_DIR = pathlib.Path(tempdir.name) +input_file = get_example_file(ExampleKeys.BASIC_FLAT_PLATE_CDB, WORKING_DIR) + +# %% +# Launch the PyACP server and connect to it. +acp = launch_acp() + +# %% +# Define the input file and instantiate an ACPWorkflow +# The ACPWorkflow class provides convenience methods which simplify the file handling. +# It automatically creates a model based on the input file. + +workflow = ACPWorkflow.from_cdb_file( + acp=acp, + cdb_file_path=input_file, + local_working_directory=WORKING_DIR, +) + +model = workflow.model +print(workflow.working_directory.path) +print(model.unit_system) + +# %% +# Visualize the loaded mesh +mesh = model.mesh.to_pyvista() +mesh.plot(show_edges=True) + + +# %% +# Create the UD material and its corresponding fabric +engineering_constants_ud = ConstantEngineeringConstants.from_orthotropic_constants( + E1=5e10, E2=1e10, E3=1e10, nu12=0.28, nu13=0.28, nu23=0.3, G12=5e9, G23=4e9, G31=4e9 +) + +ud_material = model.create_material( + name="UD", + ply_type=PlyType.REGULAR, + engineering_constants=engineering_constants_ud, +) + +ud_fabric = model.create_fabric(name="UD", material=ud_material, thickness=0.002) + +# %% +# Define a rosette and an oriented selection set +rosette = model.create_rosette(origin=(0.0, 0.0, 0.0), dir1=(1.0, 0.0, 0.0), dir2=(0.0, 1.0, 0.0)) + +oss = model.create_oriented_selection_set( + name="oss", + orientation_point=(0.0, 0.0, 0.0), + orientation_direction=(0.0, 1.0, 0), + element_sets=[model.element_sets["All_Elements"]], + rosettes=[rosette], +) + +# %% +# Create a ply with an attached parallel selection rule and plot the ply extent +modeling_group = model.create_modeling_group(name="modeling_group") + +parallel_rule = model.create_parallel_selection_rule( + name="parallel_rule", + origin=(0, 0, 0), + direction=(1, 0, 0), + lower_limit=0.005, + upper_limit=1, +) + +linked_parallel_rule = LinkedSelectionRule(parallel_rule) +partial_ply = modeling_group.create_modeling_ply( + name="partial_ply", + ply_angle=90, + ply_material=ud_fabric, + oriented_selection_sets=[oss], + selection_rules=[linked_parallel_rule], + number_of_layers=10, +) + +model.update() +assert model.elemental_data.thickness is not None +model.elemental_data.thickness.get_pyvista_mesh(mesh=model.mesh).plot(show_edges=True) + + +# %% +# Rules can be parametrized. This makes sense when a rule is used multiple times with different +# parameters. :class:`.LinkedSelectionRule` shows what parameters are available for each rule. +# Here, the extent of the parallel rule modified. +linked_parallel_rule.template_rule = True +linked_parallel_rule.parameter_1 = 0.002 +linked_parallel_rule.parameter_2 = 0.1 + +model.update() +assert model.elemental_data.thickness is not None +model.elemental_data.thickness.get_pyvista_mesh(mesh=model.mesh).plot(show_edges=True) + + +# %% +# Create a geometrical selection rule + +# Add CAD geometry to the model. +triangle_path = example_helpers.get_example_file( + example_helpers.ExampleKeys.RULE_GEOMETRY_TRIANGLE, WORKING_DIR +) +triangle = workflow.add_cad_geometry_from_local_file(triangle_path) + + +# Note: It is important to update the model here, because the root_shapes of the +# cad_geometry are not available until the model is updated. +model.update() + +# Create a virtual geometry from the CAD geometry +triangle_virtual_geometry = model.create_virtual_geometry( + name="triangle_virtual_geometry", cad_components=triangle.root_shapes.values() +) + +# Create the geometrical selection rule +geometrical_selection_rule = model.create_geometrical_selection_rule( + name="geometrical_rule", + geometry=triangle_virtual_geometry, +) + +# Assign the geometrical selection rule to the ply and plot the ply extend with +# the outline of the geometry +partial_ply.selection_rules = [LinkedSelectionRule(geometrical_selection_rule)] +model.update() +assert model.elemental_data.thickness is not None + +plotter = pyvista.Plotter() +plotter.add_mesh( + triangle.visualization_mesh.to_pyvista(), style="wireframe", line_width=4, color="white" +) +plotter.add_mesh(model.elemental_data.thickness.get_pyvista_mesh(mesh=model.mesh), show_edges=True) +plotter.show() + +# %% +# Create a cutoff selection rule + +# Add the cutoff CAD geometry to the model +cutoff_plane_path = example_helpers.get_example_file( + example_helpers.ExampleKeys.CUT_OFF_GEOMETRY, WORKING_DIR +) +cut_off_plane = workflow.add_cad_geometry_from_local_file(cutoff_plane_path) + +# Note: It is important to update the model here, because the root_shapes of the +# cad_geometry are not available until the model is updated. +model.update() + +# Create a virtual geometry from the CAD geometry +cutoff_virtual_geometry = model.create_virtual_geometry( + name="cutoff_virtual_geometry", cad_components=cut_off_plane.root_shapes.values() +) + +# Create the cutoff selection rule +cutoff_selection_rule = model.create_cutoff_selection_rule( + name="cutoff_rule", + cutoff_geometry=cutoff_virtual_geometry, +) + +partial_ply.selection_rules = [LinkedSelectionRule(cutoff_selection_rule)] + +model.update() +assert model.elemental_data.thickness is not None + +# Plot the ply extent together with the cutoff geometry +plotter = pyvista.Plotter() +plotter.add_mesh(cut_off_plane.visualization_mesh.to_pyvista(), color="white") +plotter.add_mesh(model.elemental_data.thickness.get_pyvista_mesh(mesh=model.mesh)) +plotter.camera_position = [(-0.05, 0.01, 0), (0.005, 0.005, 0.005), (0, 1, 0)] + +plotter.show() + +# %% +# Create a variable offset selection rule + +# Create the lookup table +lookup_table = model.create_lookup_table_1d( + name="lookup_table", + origin=(0, 0, 0), + direction=(0, 0, 1), +) + +# Add the location data. The "Location" column of the lookup table +# is always created by default. +lookup_table.columns["Location"].data = np.array([0, 0.005, 0.01]) + +# Create the offset column that defines the offsets from the edge. +offsets_column = lookup_table.create_column( + name="offset", + dimension_type=DimensionType.LENGTH, + data=np.array([0.00, 0.004, 0]), +) + +# Create the edge set from the "All_Elements" element set. Because we set +# the limit angle to 30°, only one edge at x=0 will be selected. +edge_set = model.create_edge_set( + name="edge_set", + edge_set_type=EdgeSetType.BY_REFERENCE, + limit_angle=30, + element_set=model.element_sets["All_Elements"], + origin=(0, 0, 0), +) + +# Create the variable offset rule and assign it to the ply +variable_offset_rule = model.create_variable_offset_selection_rule( + name="variable_offset_rule", edge_set=edge_set, offsets=offsets_column, distance_along_edge=True +) + +partial_ply.selection_rules = [LinkedSelectionRule(variable_offset_rule)] + +# Plot the ply extent +model.update() +assert model.elemental_data.thickness is not None +model.elemental_data.thickness.get_pyvista_mesh(mesh=model.mesh).plot(show_edges=True) + +# %% +# Create a boolean selection rule. +# Note: Creating a boolean selection rule and assigning it to a ply has the same +# effect as linking the individual rules to the ply directly. Boolean rules are still useful +# because they help to organize the rules and can be used to create more complex rules. + +# Create a cylindrical selection rule which will be combined with the parallel rule. +cylindrical_rule_boolean = model.create_cylindrical_selection_rule( + name="cylindrical_rule", + origin=(0.005, 0, 0.005), + direction=(0, 1, 0), + radius=0.002, +) + +parallel_rule_boolean = model.create_parallel_selection_rule( + name="parallel_rule", + origin=(0, 0, 0), + direction=(1, 0, 0), + lower_limit=0.005, + upper_limit=1, +) + +linked_cylindrical_rule_boolean = LinkedSelectionRule(cylindrical_rule_boolean) +linked_parallel_rule_boolean = LinkedSelectionRule(parallel_rule_boolean) + +boolean_selection_rule = model.create_boolean_selection_rule( + name="boolean_rule", + selection_rules=[linked_parallel_rule_boolean, linked_cylindrical_rule_boolean], +) + +partial_ply.selection_rules = [LinkedSelectionRule(boolean_selection_rule)] + +# Plot the ply extent +model.update() +assert model.elemental_data.thickness is not None +model.elemental_data.thickness.get_pyvista_mesh(mesh=model.mesh).plot(show_edges=True) + +# %% +# Modify the operation type of the boolean selection rule +linked_parallel_rule_boolean.operation_type = BooleanOperationType.INTERSECT +linked_cylindrical_rule_boolean.operation_type = BooleanOperationType.ADD + +# Plot the ply extent +model.update() +assert model.elemental_data.thickness is not None +model.elemental_data.thickness.get_pyvista_mesh(mesh=model.mesh).plot(show_edges=True) diff --git a/src/ansys/acp/core/_tree_objects/linked_selection_rule.py b/src/ansys/acp/core/_tree_objects/linked_selection_rule.py index 10433b79af..e4c414617f 100644 --- a/src/ansys/acp/core/_tree_objects/linked_selection_rule.py +++ b/src/ansys/acp/core/_tree_objects/linked_selection_rule.py @@ -185,10 +185,7 @@ def _from_pb_object( VariableOffsetSelectionRule, ] if not isinstance(parent_object, BooleanSelectionRule): - allowed_types_list += [ - CutoffSelectionRule, - BooleanSelectionRule, - ] + allowed_types_list += [CutoffSelectionRule, BooleanSelectionRule] allowed_types = tuple(allowed_types_list) selection_rule = tree_object_from_resource_path( diff --git a/src/ansys/acp/core/_tree_objects/oriented_selection_set.py b/src/ansys/acp/core/_tree_objects/oriented_selection_set.py index 6056774f1f..5997608556 100644 --- a/src/ansys/acp/core/_tree_objects/oriented_selection_set.py +++ b/src/ansys/acp/core/_tree_objects/oriented_selection_set.py @@ -2,6 +2,7 @@ from collections.abc import Iterable, Sequence import dataclasses +import typing from ansys.api.acp.v0 import oriented_selection_set_pb2, oriented_selection_set_pb2_grpc @@ -54,6 +55,21 @@ "OrientedSelectionSetNodalData", ] +if typing.TYPE_CHECKING: + # Since the 'LinkedSelectionRule' class is used by the boolean selection rule, + # this would cause a circular import at run-time. + from .. import BooleanSelectionRule, GeometricalSelectionRule + + _SELECTION_RULES_LINKABLE_TO_OSS = typing.Union[ + ParallelSelectionRule, + CylindricalSelectionRule, + SphericalSelectionRule, + TubeSelectionRule, + GeometricalSelectionRule, + VariableOffsetSelectionRule, + BooleanSelectionRule, + ] + @dataclasses.dataclass class OrientedSelectionSetElementalData(ElementalData): @@ -127,6 +143,7 @@ def __init__( orientation_direction: tuple[float, float, float] = (0.0, 0.0, 0.0), rosettes: Sequence[Rosette] = tuple(), rosette_selection_method: RosetteSelectionMethod = "minimum_angle", + selection_rules: Sequence[_SELECTION_RULES_LINKABLE_TO_OSS] = tuple(), draping: bool = False, draping_seed_point: tuple[float, float, float] = (0.0, 0.0, 0.0), auto_draping_direction: bool = True, @@ -143,6 +160,7 @@ def __init__( self.orientation_direction = orientation_direction self.rosettes = rosettes self.rosette_selection_method = RosetteSelectionMethod(rosette_selection_method) + self.selection_rules = selection_rules self.draping = draping self.draping_seed_point = draping_seed_point self.auto_draping_direction = auto_draping_direction diff --git a/src/ansys/acp/core/_tree_objects/virtual_geometry.py b/src/ansys/acp/core/_tree_objects/virtual_geometry.py index 100350cf7b..b8bf4b58f5 100644 --- a/src/ansys/acp/core/_tree_objects/virtual_geometry.py +++ b/src/ansys/acp/core/_tree_objects/virtual_geometry.py @@ -93,14 +93,18 @@ def __repr__(self) -> str: class VirtualGeometry(CreatableTreeObject, IdTreeObject): """Instantiate a Virtual Geometry. + The virtual geometry can be created from a set of CAD components or from a set of SubShapes. + Combining CAD Components and SubShapes is not allowed. + Parameters ---------- name : Name of the Virtual Geometry. - dimension : - Dimension of the Virtual Geometry, if it is uniquely defined. + cad_components : + CAD Components that make up the virtual geometry. sub_shapes : - Paths of the CAD Components that make up the virtual geometry. + SubShapes that make up the virtual geometry. + """ __slots__: Iterable[str] = tuple() @@ -112,12 +116,24 @@ def __init__( self, *, name: str = "VirtualGeometry", - sub_shapes: Iterable[str] = (), + cad_components: Iterable[CADComponent] | None = None, + sub_shapes: Iterable[SubShape] | None = None, ): super().__init__( name=name, ) - self.sub_shapes = sub_shapes + + if cad_components is not None and sub_shapes is not None: + raise ValueError("cad_components and sub_shapes cannot be set at the same time") + + if cad_components is None and sub_shapes is None: + self.sub_shapes = [] + + if cad_components is not None: + self.set_cad_components(cad_components=cad_components) + + if sub_shapes is not None: + self.sub_shapes = sub_shapes def _create_stub(self) -> virtual_geometry_pb2_grpc.ObjectServiceStub: return virtual_geometry_pb2_grpc.ObjectServiceStub(self._channel) @@ -126,6 +142,7 @@ def _create_stub(self) -> virtual_geometry_pb2_grpc.ObjectServiceStub: dimension = grpc_data_property_read_only( "properties.dimension", from_protobuf=virtual_geometry_dimension_from_pb ) + sub_shapes = define_edge_property_list( "properties.sub_shapes", SubShape, diff --git a/src/ansys/acp/core/example_helpers.py b/src/ansys/acp/core/example_helpers.py index 8a3c8ba132..168b4631f5 100644 --- a/src/ansys/acp/core/example_helpers.py +++ b/src/ansys/acp/core/example_helpers.py @@ -11,6 +11,11 @@ __all__ = ["ExampleKeys", "get_example_file"] +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ansys.acp.core import ACPWorkflow + _EXAMPLE_REPO = "https://github.com/ansys/example-data/raw/master/pyacp/" @@ -27,6 +32,8 @@ class ExampleKeys(Enum): BASIC_FLAT_PLATE_ACPH5 = auto() RACE_CAR_NOSE_ACPH5 = auto() RACE_CAR_NOSE_STEP = auto() + CUT_OFF_GEOMETRY = auto() + RULE_GEOMETRY_TRIANGLE = auto() EXAMPLE_FILES: dict[ExampleKeys, _ExampleLocation] = { @@ -42,6 +49,12 @@ class ExampleKeys(Enum): ExampleKeys.RACE_CAR_NOSE_STEP: _ExampleLocation( directory="race_car_nose", filename="race_car_nose.stp" ), + ExampleKeys.CUT_OFF_GEOMETRY: _ExampleLocation( + directory="geometries", filename="cut_off_geometry.stp" + ), + ExampleKeys.RULE_GEOMETRY_TRIANGLE: _ExampleLocation( + directory="geometries", filename="rule_geometry_triangle.stp" + ), } @@ -67,3 +80,61 @@ def _get_file_url(example_location: _ExampleLocation) -> str: def _download_file(example_location: _ExampleLocation, local_path: pathlib.Path) -> None: file_url = _get_file_url(example_location) urllib.request.urlretrieve(file_url, local_path) + + +def _run_analysis(workflow: "ACPWorkflow") -> None: + """Run the model with mapdl and do a post-processing analysis. + + Uses a max strain criteria, which means strain limits have to be defined. + This function can be called in the end of examples to verify the prepared model + actually solves and can be post-processed. + """ + from ansys.mapdl.core import launch_mapdl + + model = workflow.model + model.update() + + # Launch the MAPDL instance + mapdl = launch_mapdl() + mapdl.clear() + + # Load the CDB file into PyMAPDL + mapdl.input(str(workflow.get_local_cdb_file())) + + # Solve the model + mapdl.allsel() + mapdl.slashsolu() + mapdl.solve() + + # Download the rst file for composite specific post-processing + rstfile_name = f"{mapdl.jobname}.rst" + rst_file_local_path = workflow.working_directory.path / rstfile_name + mapdl.download(rstfile_name, str(workflow.working_directory.path)) + + from ansys.acp.core import get_composite_post_processing_files, get_dpf_unit_system + from ansys.dpf.composites.composite_model import CompositeModel + from ansys.dpf.composites.constants import FailureOutput + from ansys.dpf.composites.failure_criteria import CombinedFailureCriterion, MaxStrainCriterion + from ansys.dpf.composites.server_helpers import connect_to_or_start_server + + dpf_server = connect_to_or_start_server() + + max_strain = MaxStrainCriterion() + + cfc = CombinedFailureCriterion( + name="Combined Failure Criterion", + failure_criteria=[max_strain], + ) + + composite_model = CompositeModel( + get_composite_post_processing_files(workflow, rst_file_local_path), + default_unit_system=get_dpf_unit_system(model.unit_system), + server=dpf_server, + ) + + output_all_elements = composite_model.evaluate_failure_criteria(cfc) + irf_field = output_all_elements.get_field({"failure_label": FailureOutput.FAILURE_VALUE}) + + # %% + # Release composite model to close open streams to result file. + composite_model = None # type: ignore diff --git a/tests/unittests/common/tree_object_tester.py b/tests/unittests/common/tree_object_tester.py index 4d93c2a0be..9d4d33d831 100644 --- a/tests/unittests/common/tree_object_tester.py +++ b/tests/unittests/common/tree_object_tester.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Any +from typing import Any, Optional import pytest @@ -10,6 +10,14 @@ class ObjectPropertiesToTest: read_write: list[tuple[str, Any]] read_only: list[tuple[str, Any]] + # If create_args do not exist, the create method will use read-write properties + # to create the object + # Note: the read-write properties can contain the same property twice, with different values. + # The last value will be used to create the object. + # If create_args exists the create method will use the create_args dictionaries + # to create the object. The object + # will be created for each dictionary in the list. + create_args: Optional[list[dict[str, Any]]] = None class TreeObjectTesterReadOnly: @@ -76,23 +84,49 @@ def test_collection_getitem_inexistent(collection_test_data): class TreeObjectTester(TreeObjectTesterReadOnly): COLLECTION_NAME: str - DEFAULT_PROPERTIES: dict[str, Any] CREATE_METHOD_NAME: str - def test_create(self, parent_object): - """Test the creation of objects.""" + def test_create_with_default_arguments(self, parent_object, default_properties): + """Test the creation of objects with default arguments.""" create_method = getattr(parent_object, self.CREATE_METHOD_NAME) names = ["ObjectName.1", "ObjectName.1", "üñıçよð€"] for ref_name in names: new_object = create_method(name=ref_name) assert new_object.name == ref_name - for key, val in self.DEFAULT_PROPERTIES.items(): + for key, val in default_properties.items(): assert_allclose( actual=getattr(new_object, key), desired=val, msg=f"Attribute {key} not set correctly. Expected {val}, got {getattr(new_object, key)}", ), + def test_create_with_defined_properties( + self, parent_object, object_properties: ObjectPropertiesToTest + ): + """Test the creation of objects with properties defined in object_properties.""" + + def create_and_check(init_args: dict[str, Any]): + create_method = getattr(parent_object, self.CREATE_METHOD_NAME) + + new_object = create_method(**init_args) + for key, val in init_args.items(): + assert_allclose( + actual=getattr(new_object, key), + desired=val, + msg=f"Attribute {key} not set correctly. Expected {val}, got {getattr(new_object, key)}", + ), + + if object_properties.create_args is None: + # Note: The object_properties.read_write can contain the same property twice, + # with different values. By converting it to a dictionary, we just keep the last value + # that was set and use it to construct the object. + init_args = {key: value for key, value in object_properties.read_write} + create_and_check(init_args) + else: + assert object_properties.create_args is not None + for init_args in object_properties.create_args: + create_and_check(init_args) + @staticmethod def test_properties(tree_object, object_properties: ObjectPropertiesToTest): for prop, value in object_properties.read_write: diff --git a/tests/unittests/test_boolean_selection_rule.py b/tests/unittests/test_boolean_selection_rule.py index 56ffbd319e..ec59114f98 100644 --- a/tests/unittests/test_boolean_selection_rule.py +++ b/tests/unittests/test_boolean_selection_rule.py @@ -23,11 +23,15 @@ def tree_object(parent_object): class TestBooleanSelectionRule(NoLockedMixin, TreeObjectTester): COLLECTION_NAME = "boolean_selection_rules" - DEFAULT_PROPERTIES = { - "status": "NOTUPTODATE", - "selection_rules": [], - "include_rule_type": True, - } + + @staticmethod + @pytest.fixture + def default_properties(): + return { + "status": "NOTUPTODATE", + "selection_rules": [], + "include_rule_type": True, + } CREATE_METHOD_NAME = "create_boolean_selection_rule" diff --git a/tests/unittests/test_cad_geometry.py b/tests/unittests/test_cad_geometry.py index 716d807c58..bcb312ecd8 100644 --- a/tests/unittests/test_cad_geometry.py +++ b/tests/unittests/test_cad_geometry.py @@ -16,15 +16,19 @@ def tree_object(parent_object): class TestCADGeometry(NoLockedMixin, TreeObjectTester): COLLECTION_NAME = "cad_geometries" - DEFAULT_PROPERTIES = { - "status": "NOTUPTODATE", - "external_path": "", - "scale_factor": 1.0, - "use_default_precision": True, - "precision": 1e-3, - "use_default_offset": True, - "offset": 0.0, - } + + @staticmethod + @pytest.fixture + def default_properties(): + return { + "status": "NOTUPTODATE", + "external_path": "", + "scale_factor": 1.0, + "use_default_precision": True, + "precision": 1e-3, + "use_default_offset": True, + "offset": 0.0, + } CREATE_METHOD_NAME = "create_cad_geometry" diff --git a/tests/unittests/test_cutoff_selection_rule.py b/tests/unittests/test_cutoff_selection_rule.py index 06867f27ad..1477cda8bf 100644 --- a/tests/unittests/test_cutoff_selection_rule.py +++ b/tests/unittests/test_cutoff_selection_rule.py @@ -23,16 +23,20 @@ def tree_object(parent_object): class TestCutoffSelectionRule(NoLockedMixin, TreeObjectTester): COLLECTION_NAME = "cutoff_selection_rules" - DEFAULT_PROPERTIES = { - "status": "NOTUPTODATE", - "cutoff_rule_type": CutoffRuleType.GEOMETRY, - "cutoff_geometry": None, - "taper_edge_set": None, - "offset": 0.0, - "angle": 0.0, - "ply_cutoff_type": PlyCutoffType.PRODUCTION_PLY_CUTOFF, - "ply_tapering": False, - } + + @staticmethod + @pytest.fixture + def default_properties(): + return { + "status": "NOTUPTODATE", + "cutoff_rule_type": CutoffRuleType.GEOMETRY, + "cutoff_geometry": None, + "taper_edge_set": None, + "offset": 0.0, + "angle": 0.0, + "ply_cutoff_type": PlyCutoffType.PRODUCTION_PLY_CUTOFF, + "ply_tapering": False, + } CREATE_METHOD_NAME = "create_cutoff_selection_rule" diff --git a/tests/unittests/test_cylindrical_selection_rule.py b/tests/unittests/test_cylindrical_selection_rule.py index ac23977e47..1d345f4220 100644 --- a/tests/unittests/test_cylindrical_selection_rule.py +++ b/tests/unittests/test_cylindrical_selection_rule.py @@ -18,16 +18,20 @@ def tree_object(parent_object): class TestCylindricalSelectionRule(NoLockedMixin, TreeObjectTester): COLLECTION_NAME = "cylindrical_selection_rules" - DEFAULT_PROPERTIES = { - "status": "NOTUPTODATE", - "use_global_coordinate_system": True, - "rosette": None, - "origin": (0.0, 0.0, 0.0), - "direction": (0.0, 0.0, 1.0), - "radius": 0.0, - "relative_rule_type": False, - "include_rule_type": True, - } + + @staticmethod + @pytest.fixture + def default_properties(): + return { + "status": "NOTUPTODATE", + "use_global_coordinate_system": True, + "rosette": None, + "origin": (0.0, 0.0, 0.0), + "direction": (0.0, 0.0, 1.0), + "radius": 0.0, + "relative_rule_type": False, + "include_rule_type": True, + } CREATE_METHOD_NAME = "create_cylindrical_selection_rule" diff --git a/tests/unittests/test_edge_set.py b/tests/unittests/test_edge_set.py index a49b49b2ac..5bd36e9e29 100644 --- a/tests/unittests/test_edge_set.py +++ b/tests/unittests/test_edge_set.py @@ -18,14 +18,18 @@ def tree_object(parent_object): class TestEdgeSet(NoLockedMixin, TreeObjectTester): COLLECTION_NAME = "edge_sets" - DEFAULT_PROPERTIES = { - "status": "NOTUPTODATE", - "edge_set_type": EdgeSetType.BY_REFERENCE, - "element_set": None, - "defining_node_labels": tuple(), - "limit_angle": -1.0, - "origin": (0.0, 0.0, 0.0), - } + + @staticmethod + @pytest.fixture + def default_properties(): + return { + "status": "NOTUPTODATE", + "edge_set_type": EdgeSetType.BY_REFERENCE, + "element_set": None, + "defining_node_labels": tuple(), + "limit_angle": -1.0, + "origin": (0.0, 0.0, 0.0), + } CREATE_METHOD_NAME = "create_edge_set" diff --git a/tests/unittests/test_element_set.py b/tests/unittests/test_element_set.py index 5a85b6dbc9..4415b0508e 100644 --- a/tests/unittests/test_element_set.py +++ b/tests/unittests/test_element_set.py @@ -31,11 +31,16 @@ def object_properties(): class TestElementSet(WithLockedMixin, TreeObjectTester): COLLECTION_NAME = "element_sets" - DEFAULT_PROPERTIES = { - "status": "NOTUPTODATE", - "locked": False, - "middle_offset": False, - "element_labels": tuple(), - } + + @staticmethod + @pytest.fixture + def default_properties(): + return { + "status": "NOTUPTODATE", + "locked": False, + "middle_offset": False, + "element_labels": tuple(), + } + CREATE_METHOD_NAME = "create_element_set" INITIAL_OBJECT_NAMES = ("All_Elements",) diff --git a/tests/unittests/test_fabric.py b/tests/unittests/test_fabric.py index e47cd7d92d..9828e36b69 100644 --- a/tests/unittests/test_fabric.py +++ b/tests/unittests/test_fabric.py @@ -18,17 +18,21 @@ def tree_object(parent_object): class TestFabric(NoLockedMixin, TreeObjectTester): COLLECTION_NAME = "fabrics" - DEFAULT_PROPERTIES = { - "status": "NOTUPTODATE", - "thickness": 0.0, - "area_price": 0.0, - "ignore_for_postprocessing": False, - "drop_off_material_handling": DropoffMaterialType.GLOBAL, - "cut_off_material_handling": CutoffMaterialType.COMPUTED, - "draping_material_model": DrapingMaterialType.WOVEN, - "draping_ud_coefficient": 0.0, - "material": None, - } + + @staticmethod + @pytest.fixture + def default_properties(): + return { + "status": "NOTUPTODATE", + "thickness": 0.0, + "area_price": 0.0, + "ignore_for_postprocessing": False, + "drop_off_material_handling": DropoffMaterialType.GLOBAL, + "cut_off_material_handling": CutoffMaterialType.COMPUTED, + "draping_material_model": DrapingMaterialType.WOVEN, + "draping_ud_coefficient": 0.0, + "material": None, + } CREATE_METHOD_NAME = "create_fabric" diff --git a/tests/unittests/test_geometrical_selection_rule.py b/tests/unittests/test_geometrical_selection_rule.py index 52a9bcdbdf..b18edf5b03 100644 --- a/tests/unittests/test_geometrical_selection_rule.py +++ b/tests/unittests/test_geometrical_selection_rule.py @@ -22,17 +22,21 @@ def tree_object(parent_object): class TestGeometricalSelectionRule(NoLockedMixin, TreeObjectTester): COLLECTION_NAME = "geometrical_selection_rules" - DEFAULT_PROPERTIES = { - "status": "NOTUPTODATE", - "geometrical_rule_type": GeometricalRuleType.GEOMETRY, - "geometry": None, - "element_sets": [], - "include_rule_type": True, - "use_default_tolerances": True, - "in_plane_capture_tolerance": 0.0, - "negative_capture_tolerance": 0.0, - "positive_capture_tolerance": 0.0, - } + + @staticmethod + @pytest.fixture + def default_properties(): + return { + "status": "NOTUPTODATE", + "geometrical_rule_type": GeometricalRuleType.GEOMETRY, + "geometry": None, + "element_sets": [], + "include_rule_type": True, + "use_default_tolerances": True, + "in_plane_capture_tolerance": 0.0, + "negative_capture_tolerance": 0.0, + "positive_capture_tolerance": 0.0, + } CREATE_METHOD_NAME = "create_geometrical_selection_rule" diff --git a/tests/unittests/test_lookup_table_1d.py b/tests/unittests/test_lookup_table_1d.py index 292345f881..1c1ee5b73a 100644 --- a/tests/unittests/test_lookup_table_1d.py +++ b/tests/unittests/test_lookup_table_1d.py @@ -16,11 +16,15 @@ def tree_object(parent_object): class TestLookUpTable1D(NoLockedMixin, TreeObjectTester): COLLECTION_NAME = "lookup_tables_1d" - DEFAULT_PROPERTIES = { - "status": "NOTUPTODATE", - "origin": (0.0, 0.0, 0.0), - "direction": (0.0, 0.0, 0.0), - } + + @staticmethod + @pytest.fixture + def default_properties(): + return { + "status": "NOTUPTODATE", + "origin": (0.0, 0.0, 0.0), + "direction": (0.0, 0.0, 0.0), + } CREATE_METHOD_NAME = "create_lookup_table_1d" diff --git a/tests/unittests/test_lookup_table_3d.py b/tests/unittests/test_lookup_table_3d.py index 01716c34e1..787c4850ec 100644 --- a/tests/unittests/test_lookup_table_3d.py +++ b/tests/unittests/test_lookup_table_3d.py @@ -18,13 +18,17 @@ def tree_object(parent_object): class TestLookUpTable1D(NoLockedMixin, TreeObjectTester): COLLECTION_NAME = "lookup_tables_3d" - DEFAULT_PROPERTIES = { - "status": "NOTUPTODATE", - "interpolation_algorithm": LookUpTable3DInterpolationAlgorithm.WEIGHTED_NEAREST_NEIGHBOR, - "use_default_search_radius": True, - "search_radius": 0.0, - "num_min_neighbors": 1, - } + + @staticmethod + @pytest.fixture + def default_properties(): + return { + "status": "NOTUPTODATE", + "interpolation_algorithm": LookUpTable3DInterpolationAlgorithm.WEIGHTED_NEAREST_NEIGHBOR, + "use_default_search_radius": True, + "search_radius": 0.0, + "num_min_neighbors": 1, + } CREATE_METHOD_NAME = "create_lookup_table_3d" diff --git a/tests/unittests/test_lookup_table_column.py b/tests/unittests/test_lookup_table_column.py index 0dd8f8c27f..99df96daab 100644 --- a/tests/unittests/test_lookup_table_column.py +++ b/tests/unittests/test_lookup_table_column.py @@ -25,10 +25,17 @@ def column_type_to_test(request): @pytest.fixture -def parent_object(parent_model, column_type_to_test): +def parent_object(parent_model, column_type_to_test, num_points): + # Note: We need to add dummy values to the location column. Otherwise + # a "shape mismatch" error occurs when we try to create columns + # with data. if column_type_to_test == LookUpTable1DColumn: - return parent_model.create_lookup_table_1d() - return parent_model.create_lookup_table_3d() + lookup_table = parent_model.create_lookup_table_1d() + lookup_table.columns["Location"].data = np.random.rand(num_points) + return lookup_table + lookup_table = parent_model.create_lookup_table_3d() + lookup_table.columns["Location"].data = np.random.rand(num_points, 3) + return lookup_table @pytest.fixture(params=[0, 1, 5]) @@ -64,19 +71,32 @@ def tree_object(parent_object, column_value_type, num_points, column_type_to_tes return parent_object.create_column(value_type=column_value_type) +@pytest.fixture +def default_data(num_points): + # Default data is a NaN array with the same number of entries as as the location column. + # The default value_type is LookUpTableColumnValueType.SCALAR, so we have one + # scalar value per location + return np.full(num_points, np.nan) + + class TestLookUpTableColumn(WithLockedMixin, TreeObjectTester): INITIAL_OBJECT_NAMES = ("Location",) COLLECTION_NAME = "columns" - DEFAULT_PROPERTIES = { - "value_type": LookUpTableColumnValueType.SCALAR, - "dimension_type": DimensionType.DIMENSIONLESS, - "data": np.array([]), - } + + @staticmethod + @pytest.fixture + def default_properties(default_data): + return { + "value_type": LookUpTableColumnValueType.SCALAR, + "dimension_type": DimensionType.DIMENSIONLESS, + "data": default_data, + } + CREATE_METHOD_NAME = "create_column" @staticmethod @pytest.fixture - def object_properties(column_data): + def object_properties(column_data, column_value_type): return ObjectPropertiesToTest( read_write=[ ("name", "some_name"), @@ -90,4 +110,12 @@ def object_properties(column_data): ("value_type", LookUpTableColumnValueType.SCALAR), ("value_type", LookUpTableColumnValueType.DIRECTION), ], + create_args=[ + { + "name": "some_name", + "data": column_data, + "dimension_type": DimensionType.TIME, + "value_type": column_value_type, + } + ], ) diff --git a/tests/unittests/test_material.py b/tests/unittests/test_material.py index 31094e8bd4..2515c22947 100644 --- a/tests/unittests/test_material.py +++ b/tests/unittests/test_material.py @@ -63,9 +63,14 @@ def object_properties(): class TestMaterial(WithLockedMixin, TreeObjectTester): COLLECTION_NAME = "materials" - DEFAULT_PROPERTIES = { - "ply_type": PlyType.UNDEFINED, - } + + @staticmethod + @pytest.fixture + def default_properties(): + return { + "ply_type": PlyType.UNDEFINED, + } + CREATE_METHOD_NAME = "create_material" INITIAL_OBJECT_NAMES = ("Structural Steel",) DEFAULT_VALUES_BY_PROPERTY_SET = { diff --git a/tests/unittests/test_modeling_group.py b/tests/unittests/test_modeling_group.py index beac724fcf..5450e48d08 100644 --- a/tests/unittests/test_modeling_group.py +++ b/tests/unittests/test_modeling_group.py @@ -16,7 +16,12 @@ def tree_object(parent_object): class TestModelingGroup(NoLockedMixin, TreeObjectTester): COLLECTION_NAME = "modeling_groups" - DEFAULT_PROPERTIES = {} + + @staticmethod + @pytest.fixture + def default_properties(): + return {} + CREATE_METHOD_NAME = "create_modeling_group" @staticmethod diff --git a/tests/unittests/test_modeling_ply.py b/tests/unittests/test_modeling_ply.py index 86f2faff43..abc79af668 100644 --- a/tests/unittests/test_modeling_ply.py +++ b/tests/unittests/test_modeling_ply.py @@ -42,28 +42,33 @@ def tree_object(parent_object): class TestModelingPly(NoLockedMixin, TreeObjectTester): COLLECTION_NAME = "modeling_plies" - DEFAULT_PROPERTIES = { - "status": "NOTUPTODATE", - "oriented_selection_sets": [], - "ply_material": None, - "ply_angle": 0.0, - "active": True, - "global_ply_nr": AnyThing(), - "draping": DrapingType.NO_DRAPING, - "draping_seed_point": (0.0, 0.0, 0.0), - "auto_draping_direction": True, - "draping_direction": (1.0, 0.0, 0.0), - "use_default_draping_mesh_size": True, - "draping_mesh_size": 0.0, - "draping_thickness_correction": True, - "draping_angle_1_field": None, - "draping_angle_2_field": None, - "thickness_type": ThicknessType.NOMINAL, - "thickness_geometry": None, - "thickness_field": None, - "thickness_field_type": ThicknessFieldType.ABSOLUTE_VALUES, - "taper_edges": [], - } + + @staticmethod + @pytest.fixture + def default_properties(): + return { + "status": "NOTUPTODATE", + "oriented_selection_sets": [], + "ply_material": None, + "ply_angle": 0.0, + "active": True, + "global_ply_nr": AnyThing(), + "draping": DrapingType.NO_DRAPING, + "draping_seed_point": (0.0, 0.0, 0.0), + "auto_draping_direction": True, + "draping_direction": (1.0, 0.0, 0.0), + "use_default_draping_mesh_size": True, + "draping_mesh_size": 0.0, + "draping_thickness_correction": True, + "draping_angle_1_field": None, + "draping_angle_2_field": None, + "thickness_type": ThicknessType.NOMINAL, + "thickness_geometry": None, + "thickness_field": None, + "thickness_field_type": ThicknessFieldType.ABSOLUTE_VALUES, + "taper_edges": [], + } + CREATE_METHOD_NAME = "create_modeling_ply" @staticmethod diff --git a/tests/unittests/test_oriented_selection_set.py b/tests/unittests/test_oriented_selection_set.py index 2776a6358d..049c9754b5 100644 --- a/tests/unittests/test_oriented_selection_set.py +++ b/tests/unittests/test_oriented_selection_set.py @@ -20,23 +20,28 @@ def tree_object(parent_object): class TestOrientedSelectionSet(NoLockedMixin, TreeObjectTester): COLLECTION_NAME = "oriented_selection_sets" - DEFAULT_PROPERTIES = { - "status": "NOTUPTODATE", - "element_sets": [], - "rosettes": [], - "orientation_point": (0.0, 0.0, 0.0), - "orientation_direction": (0.0, 0.0, 0.0), - "rosette_selection_method": "minimum_angle", - "draping": False, - "draping_seed_point": (0.0, 0.0, 0.0), - "auto_draping_direction": True, - "draping_direction": (0.0, 0.0, 1.0), - "use_default_draping_mesh_size": True, - "draping_mesh_size": 0.0, - "draping_material_model": "woven", - "draping_ud_coefficient": 0.0, - "rotation_angle": 0.0, - } + + @staticmethod + @pytest.fixture + def default_properties(): + return { + "status": "NOTUPTODATE", + "element_sets": [], + "rosettes": [], + "orientation_point": (0.0, 0.0, 0.0), + "orientation_direction": (0.0, 0.0, 0.0), + "rosette_selection_method": "minimum_angle", + "draping": False, + "draping_seed_point": (0.0, 0.0, 0.0), + "auto_draping_direction": True, + "draping_direction": (0.0, 0.0, 1.0), + "use_default_draping_mesh_size": True, + "draping_mesh_size": 0.0, + "draping_material_model": "woven", + "draping_ud_coefficient": 0.0, + "rotation_angle": 0.0, + } + CREATE_METHOD_NAME = "create_oriented_selection_set" @staticmethod diff --git a/tests/unittests/test_parallel_selection_rule.py b/tests/unittests/test_parallel_selection_rule.py index 5dfbe9ceaf..8207d4cd61 100644 --- a/tests/unittests/test_parallel_selection_rule.py +++ b/tests/unittests/test_parallel_selection_rule.py @@ -22,17 +22,21 @@ def tree_object(parent_object): class TestParallelSelectionRule(NoLockedMixin, TreeObjectTester): COLLECTION_NAME = "parallel_selection_rules" - DEFAULT_PROPERTIES = { - "status": "NOTUPTODATE", - "use_global_coordinate_system": True, - "rosette": None, - "origin": (0.0, 0.0, 0.0), - "direction": (1.0, 0.0, 0.0), - "lower_limit": 0.0, - "upper_limit": 0.0, - "relative_rule_type": False, - "include_rule_type": True, - } + + @staticmethod + @pytest.fixture + def default_properties(): + return { + "status": "NOTUPTODATE", + "use_global_coordinate_system": True, + "rosette": None, + "origin": (0.0, 0.0, 0.0), + "direction": (1.0, 0.0, 0.0), + "lower_limit": 0.0, + "upper_limit": 0.0, + "relative_rule_type": False, + "include_rule_type": True, + } CREATE_METHOD_NAME = "create_parallel_selection_rule" diff --git a/tests/unittests/test_rosette.py b/tests/unittests/test_rosette.py index 5493a20b93..9fb559e328 100644 --- a/tests/unittests/test_rosette.py +++ b/tests/unittests/test_rosette.py @@ -16,13 +16,18 @@ def tree_object(parent_object): class TestRosette(WithLockedMixin, TreeObjectTester): COLLECTION_NAME = "rosettes" - DEFAULT_PROPERTIES = { - "status": "NOTUPTODATE", - "locked": False, - "origin": (0.0, 0.0, 0.0), - "dir1": (1.0, 0.0, 0.0), - "dir2": (0.0, 1.0, 0.0), - } + + @staticmethod + @pytest.fixture + def default_properties(): + return { + "status": "NOTUPTODATE", + "locked": False, + "origin": (0.0, 0.0, 0.0), + "dir1": (1.0, 0.0, 0.0), + "dir2": (0.0, 1.0, 0.0), + } + CREATE_METHOD_NAME = "create_rosette" INITIAL_OBJECT_NAMES = ("Global Coordinate System",) diff --git a/tests/unittests/test_sensor.py b/tests/unittests/test_sensor.py index d604e73dc3..42ea61a52e 100644 --- a/tests/unittests/test_sensor.py +++ b/tests/unittests/test_sensor.py @@ -18,17 +18,21 @@ def tree_object(parent_object): class TestSensor(NoLockedMixin, TreeObjectTester): COLLECTION_NAME = "sensors" - DEFAULT_PROPERTIES = { - "status": "NOTUPTODATE", - "active": True, - "entities": [], - "covered_area": None, - "modeling_ply_area": None, - "production_ply_area": None, - "price": None, - "weight": None, - "center_of_gravity": None, - } + + @staticmethod + @pytest.fixture + def default_properties(): + return { + "status": "NOTUPTODATE", + "active": True, + "entities": [], + "covered_area": None, + "modeling_ply_area": None, + "production_ply_area": None, + "price": None, + "weight": None, + "center_of_gravity": None, + } CREATE_METHOD_NAME = "create_sensor" @@ -52,6 +56,7 @@ def object_properties(parent_object): ("sensor_type", SensorType.SENSOR_BY_PLIES), ("entities", [modeling_ply]), ("sensor_type", SensorType.SENSOR_BY_SOLID_MODEL), + ("entities", []), ("active", False), ], read_only=[ diff --git a/tests/unittests/test_spherical_selection_rule.py b/tests/unittests/test_spherical_selection_rule.py index d7e3aa7e3f..9306b3879b 100644 --- a/tests/unittests/test_spherical_selection_rule.py +++ b/tests/unittests/test_spherical_selection_rule.py @@ -18,15 +18,19 @@ def tree_object(parent_object): class TestSphericalSelectionRule(NoLockedMixin, TreeObjectTester): COLLECTION_NAME = "spherical_selection_rules" - DEFAULT_PROPERTIES = { - "status": "NOTUPTODATE", - "use_global_coordinate_system": True, - "rosette": None, - "origin": (0.0, 0.0, 0.0), - "radius": 0.0, - "relative_rule_type": False, - "include_rule_type": True, - } + + @staticmethod + @pytest.fixture + def default_properties(): + return { + "status": "NOTUPTODATE", + "use_global_coordinate_system": True, + "rosette": None, + "origin": (0.0, 0.0, 0.0), + "radius": 0.0, + "relative_rule_type": False, + "include_rule_type": True, + } CREATE_METHOD_NAME = "create_spherical_selection_rule" diff --git a/tests/unittests/test_stackup.py b/tests/unittests/test_stackup.py index d99fd20937..155e88a002 100644 --- a/tests/unittests/test_stackup.py +++ b/tests/unittests/test_stackup.py @@ -24,19 +24,23 @@ def tree_object(parent_object): class TestStackup(NoLockedMixin, TreeObjectTester): COLLECTION_NAME = "stackups" - DEFAULT_PROPERTIES = { - "status": "NOTUPTODATE", - "area_price": 0.0, - "topdown": True, - "fabrics": [], - "symmetry": SymmetryType.NO_SYMMETRY, - "drop_off_material_handling": DropoffMaterialType.GLOBAL, - "drop_off_material": None, - "cut_off_material_handling": CutoffMaterialType.COMPUTED, - "cut_off_material": None, - "draping_material_model": DrapingMaterialType.WOVEN, - "draping_ud_coefficient": 0.0, - } + + @staticmethod + @pytest.fixture + def default_properties(): + return { + "status": "NOTUPTODATE", + "area_price": 0.0, + "topdown": True, + "fabrics": [], + "symmetry": SymmetryType.NO_SYMMETRY, + "drop_off_material_handling": DropoffMaterialType.GLOBAL, + "drop_off_material": None, + "cut_off_material_handling": CutoffMaterialType.COMPUTED, + "cut_off_material": None, + "draping_material_model": DrapingMaterialType.WOVEN, + "draping_ud_coefficient": 0.0, + } CREATE_METHOD_NAME = "create_stackup" diff --git a/tests/unittests/test_sublaminate.py b/tests/unittests/test_sublaminate.py index 1febe11afa..5a0a5f892c 100644 --- a/tests/unittests/test_sublaminate.py +++ b/tests/unittests/test_sublaminate.py @@ -18,12 +18,16 @@ def tree_object(parent_object): class TestSubLaminate(NoLockedMixin, TreeObjectTester): COLLECTION_NAME = "sublaminates" - DEFAULT_PROPERTIES = { - "status": "NOTUPTODATE", - "topdown": True, - "materials": [], - "symmetry": SymmetryType.NO_SYMMETRY, - } + + @staticmethod + @pytest.fixture + def default_properties(): + return { + "status": "NOTUPTODATE", + "topdown": True, + "materials": [], + "symmetry": SymmetryType.NO_SYMMETRY, + } CREATE_METHOD_NAME = "create_sublaminate" diff --git a/tests/unittests/test_tube_selection_rule.py b/tests/unittests/test_tube_selection_rule.py index 75ab958664..0dcac30d9d 100644 --- a/tests/unittests/test_tube_selection_rule.py +++ b/tests/unittests/test_tube_selection_rule.py @@ -18,18 +18,22 @@ def tree_object(parent_object): class TestTubeSelectionRule(NoLockedMixin, TreeObjectTester): COLLECTION_NAME = "tube_selection_rules" - DEFAULT_PROPERTIES = { - "status": "NOTUPTODATE", - "edge_set": None, - "outer_radius": 1.0, - "inner_radius": 0.0, - "include_rule_type": True, - "extend_endings": False, - "symmetrical_extension": True, - "head": (0.0, 0.0, 0.0), - "head_extension": 0.0, - "tail_extension": 0.0, - } + + @staticmethod + @pytest.fixture + def default_properties(): + return { + "status": "NOTUPTODATE", + "edge_set": None, + "outer_radius": 1.0, + "inner_radius": 0.0, + "include_rule_type": True, + "extend_endings": False, + "symmetrical_extension": True, + "head": (0.0, 0.0, 0.0), + "head_extension": 0.0, + "tail_extension": 0.0, + } CREATE_METHOD_NAME = "create_tube_selection_rule" diff --git a/tests/unittests/test_variable_offset_selection_rule.py b/tests/unittests/test_variable_offset_selection_rule.py index f84be4e1f0..ad4d4f26de 100644 --- a/tests/unittests/test_variable_offset_selection_rule.py +++ b/tests/unittests/test_variable_offset_selection_rule.py @@ -21,19 +21,23 @@ def tree_object(parent_object): class TestVariableOffsetSelectionRule(NoLockedMixin, TreeObjectTester): COLLECTION_NAME = "variable_offset_selection_rules" - DEFAULT_PROPERTIES = { - "status": "NOTUPTODATE", - "edge_set": None, - "offsets": None, - "angles": None, - "include_rule_type": True, - "use_offset_correction": False, - "element_set": None, - "inherit_from_lookup_table": True, - "radius_origin": (0.0, 0.0, 0.0), - "radius_direction": (1.0, 0.0, 0.0), - "distance_along_edge": False, - } + + @staticmethod + @pytest.fixture + def default_properties(): + return { + "status": "NOTUPTODATE", + "edge_set": None, + "offsets": None, + "angles": None, + "include_rule_type": True, + "use_offset_correction": False, + "element_set": None, + "inherit_from_lookup_table": True, + "radius_origin": (0.0, 0.0, 0.0), + "radius_direction": (1.0, 0.0, 0.0), + "distance_along_edge": False, + } CREATE_METHOD_NAME = "create_variable_offset_selection_rule" diff --git a/tests/unittests/test_virtual_geometry.py b/tests/unittests/test_virtual_geometry.py index 9af6654a01..ab544d9f98 100644 --- a/tests/unittests/test_virtual_geometry.py +++ b/tests/unittests/test_virtual_geometry.py @@ -18,11 +18,15 @@ def tree_object(parent_object): class TestVirtualGeometry(NoLockedMixin, TreeObjectTester): COLLECTION_NAME = "virtual_geometries" - DEFAULT_PROPERTIES = { - "status": "NOTUPTODATE", - "dimension": VirtualGeometryDimension.UNKNOWN, - "sub_shapes": [], - } + + @staticmethod + @pytest.fixture + def default_properties(): + return { + "status": "NOTUPTODATE", + "dimension": VirtualGeometryDimension.UNKNOWN, + "sub_shapes": [], + } CREATE_METHOD_NAME = "create_virtual_geometry" @@ -42,3 +46,35 @@ def object_properties(parent_object): ("dimension", VirtualGeometryDimension.SOLID), ], ) + + +def test_virtual_geometry_creation_from_cad_components(parent_object, load_cad_geometry): + model = parent_object + with load_cad_geometry(model) as cad_geometry: + model.update() + virtual_geometry = model.create_virtual_geometry( + cad_components=cad_geometry.root_shapes.values() + ) + + assert len(virtual_geometry.sub_shapes) == 2 + assert virtual_geometry.sub_shapes[0].cad_geometry == cad_geometry + assert virtual_geometry.sub_shapes[0].path == "SOLID" + assert virtual_geometry.sub_shapes[1].cad_geometry == cad_geometry + assert virtual_geometry.sub_shapes[1].path == "SHELL" + + +def test_virtual_geometry_no_or_invalid_links(parent_object, load_cad_geometry): + model = parent_object + with load_cad_geometry(model) as cad_geometry: + model.update() + + # No sub_shapes or cad_components is ok + virtual_geometry_no_shapes = model.create_virtual_geometry() + assert len(virtual_geometry_no_shapes.sub_shapes) == 0 + + # Cannot specify both sub_shapes and cad_components + with pytest.raises(ValueError): + model.create_virtual_geometry( + cad_components=cad_geometry.root_shapes.values(), + sub_shapes=[SubShape(cad_geometry=cad_geometry, path="some/path/to/shape")], + )