From 18999abf3aebfed9c666fd32a7350d17ac125265 Mon Sep 17 00:00:00 2001 From: Dominik Gresch Date: Mon, 18 Nov 2024 16:44:37 +0100 Subject: [PATCH 1/2] Fix tree printer to include all object types Change the implementation of 'get_model_tree' to use the '_GRPC_PROPERTIES' class attribute, and thus include all newly-added object types in the representation. The style of the tree has changed in some ways: - The root node now shows the model name, instead of always 'Model' - Objects (name or id) are distinguished from collections by wrapping the string in single quotes - The additional nesting level for 'Materials', 'Selection Rules', and other 'logical' groupings which are not part of the PyACP hierarchy has been removed [1] [1] This change might be debatable since the ACP GUI does show this nesting level. Since the PyACP hierarchy does not include them, it would be complicated to add them in a generic way, however. To discuss: should the collection names be capitalized (as done currently), or not (matching the PyACP attribute names)? The new tree structure can be seen in the updated test cases. Partially addresses #348, interactive tree support in Jupyter notebooks is still missing. --- src/ansys/acp/core/_model_printer.py | 106 +++++---------- tests/unittests/test_tree_printer.py | 191 ++++++++++++++------------- 2 files changed, 134 insertions(+), 163 deletions(-) diff --git a/src/ansys/acp/core/_model_printer.py b/src/ansys/acp/core/_model_printer.py index aac0847418..307239f691 100644 --- a/src/ansys/acp/core/_model_printer.py +++ b/src/ansys/acp/core/_model_printer.py @@ -22,6 +22,8 @@ import os +from ._tree_objects._grpc_helpers.mapping import Mapping +from ._tree_objects.base import TreeObjectBase from ._tree_objects.model import Model from ._utils.string_manipulation import replace_underscores_and_capitalize @@ -52,20 +54,6 @@ def __str__(self, level: int | None = 0) -> str: return ret -def _add_tree_part( - tree: Node, - container_name: str, - model: Model, -) -> None: - items = list(getattr(model, container_name).items()) - if len(items) == 0: - return - container = Node(replace_underscores_and_capitalize(container_name)) - tree.children.append(container) - for entity_name, entity in items: - group_node = Node(entity_name) - container.children.append(group_node) - def print_model(model: Model) -> None: """Print a tree representation of the model. @@ -79,68 +67,42 @@ def print_model(model: Model) -> None: return print(get_model_tree(model)) -def get_model_tree(model: Model) -> Node: +def get_model_tree(model: Model, *, hide_empty: bool = True) -> Node: """Get a tree representation of the model. Returns the root node. Parameters ---------- - model: - pyACP model. + model : + ACP model. + hide_empty : + Whether to hide empty collections. """ - model_node = Node("Model") - - material_data = Node("Material Data") - model_node.children.append(material_data) - _add_tree_part(material_data, "materials", model) - _add_tree_part(material_data, "fabrics", model) - _add_tree_part(material_data, "stackups", model) - _add_tree_part(material_data, "sublaminates", model) - - _add_tree_part(model_node, "element_sets", model) - _add_tree_part(model_node, "edge_sets", model) - - geometry = Node("Geometry") - model_node.children.append(geometry) - _add_tree_part(geometry, "cad_geometries", model) - _add_tree_part(geometry, "virtual_geometries", model) - - _add_tree_part(model_node, "rosettes", model) - - lookup_table = Node("Lookup Tables") - model_node.children.append(lookup_table) - _add_tree_part(lookup_table, "lookup_tables_1d", model) - _add_tree_part(lookup_table, "lookup_tables_3d", model) - - selection_rules = Node("Selection Rules") - model_node.children.append(selection_rules) - _add_tree_part(selection_rules, "parallel_selection_rules", model) - _add_tree_part(selection_rules, "cylindrical_selection_rules", model) - _add_tree_part(selection_rules, "spherical_selection_rules", model) - _add_tree_part(selection_rules, "tube_selection_rules", model) - _add_tree_part(selection_rules, "cutoff_selection_rules", model) - _add_tree_part(selection_rules, "geometrical_selection_rules", model) - _add_tree_part(selection_rules, "variable_offset_selection_rules", model) - _add_tree_part(selection_rules, "boolean_selection_rules", model) - - _add_tree_part(model_node, "oriented_selection_sets", model) - - modeling_groups = Node("Modeling Groups") - model_node.children.append(modeling_groups) - for modeling_group_name, modeling_group in model.modeling_groups.items(): - group_node = Node(modeling_group_name) - modeling_groups.children.append(group_node) - for modeling_ply_name, modeling_ply in modeling_group.modeling_plies.items(): - modeling_ply_node = Node(modeling_ply_name) - group_node.children.append(modeling_ply_node) - for production_ply_name, production_ply in modeling_ply.production_plies.items(): - production_ply_node = Node(production_ply_name) - modeling_ply_node.children.append(production_ply_node) - for analysis_ply_name, analysis_ply in production_ply.analysis_plies.items(): - analysis_ply_node = Node(analysis_ply_name) - production_ply_node.children.append(analysis_ply_node) - - _add_tree_part(model_node, "sensors", model) - - return model_node + return _get_model_tree_impl(obj=model, hide_empty=hide_empty) + + +def _get_model_tree_impl(obj: TreeObjectBase, *, hide_empty: bool) -> Node: + obj_node = Node(repr(_name_or_id(obj))) + for attr_name in obj._GRPC_PROPERTIES: + try: + attr = getattr(obj, attr_name) + except (AttributeError, RuntimeError): + continue + if isinstance(attr, Mapping): + collection_node = Node(replace_underscores_and_capitalize(attr_name)) + obj_node.children.append(collection_node) + for child_obj in attr.values(): + collection_node.children.append( + _get_model_tree_impl(child_obj, hide_empty=hide_empty) + ) + if hide_empty and not collection_node.children: + obj_node.children.pop() + return obj_node + + +def _name_or_id(obj: TreeObjectBase) -> str: + try: + return obj.name + except AttributeError: + return obj.id # type: ignore diff --git a/tests/unittests/test_tree_printer.py b/tests/unittests/test_tree_printer.py index 198762c87a..ad53f495e6 100644 --- a/tests/unittests/test_tree_printer.py +++ b/tests/unittests/test_tree_printer.py @@ -21,50 +21,50 @@ # SOFTWARE. import os +import textwrap + +from pytest_cases import parametrize_with_cases from ansys.acp.core import get_model_tree -def test_printed_model(acp_instance, model_data_dir): - """ - Test that model tree looks correct. - """ +def case_simple_model(acp_instance, model_data_dir): input_file_path = model_data_dir / "minimal_complete_model_no_matml_link.acph5" model = acp_instance.import_model(name="minimal_complete", path=input_file_path) - model.update() - tree = get_model_tree(model) - - assert ( - os.linesep + str(tree) - == """ -Model - Material Data - Materials - Structural Steel - Fabrics - Fabric.1 - Element Sets - All_Elements - Edge Sets - ns_edge - Geometry - Rosettes - Global Coordinate System - Lookup Tables - Selection Rules - Oriented Selection Sets - OrientedSelectionSet.1 - Modeling Groups - ModelingGroup.1 - ModelingPly.1 - ProductionPly - P1L1__ModelingPly.1 -""".replace( - "\n", os.linesep - ) + return model, textwrap.dedent( + """\ + 'minimal_complete' + Materials + 'Structural Steel' + Fabrics + 'Fabric.1' + Element Sets + 'All_Elements' + Edge Sets + 'ns_edge' + Rosettes + 'Global Coordinate System' + Oriented Selection Sets + 'OrientedSelectionSet.1' + Modeling Groups + 'ModelingGroup.1' + Modeling Plies + 'ModelingPly.1' + Production Plies + 'P1__ModelingPly.1' + Analysis Plies + 'P1L1__ModelingPly.1' + """ ) + +def case_more_objects(acp_instance, model_data_dir): + input_file_path = model_data_dir / "minimal_complete_model_no_matml_link.acph5" + model = acp_instance.import_model(name="minimal_complete", path=input_file_path) + + model.update() + model.create_edge_set() model.create_stackup() model.create_sublaminate() @@ -80,61 +80,70 @@ def test_printed_model(acp_instance, model_data_dir): model.create_lookup_table_3d() model.create_sensor() + return model, textwrap.dedent( + """\ + 'minimal_complete' + Materials + 'Structural Steel' + Fabrics + 'Fabric.1' + Stackups + 'Stackup' + Sublaminates + 'SubLaminate' + Element Sets + 'All_Elements' + Edge Sets + 'ns_edge' + 'EdgeSet' + Cad Geometries + 'CADGeometry' + Virtual Geometries + 'VirtualGeometry' + Rosettes + 'Global Coordinate System' + Lookup Tables 1d + 'LookUpTable1D' + Columns + 'Location' + Lookup Tables 3d + 'LookUpTable3D' + Columns + 'Location' + Parallel Selection Rules + 'ParallelSelectionrule' + Cylindrical Selection Rules + 'CylindricalSelectionrule' + Tube Selection Rules + 'TubeSelectionrule' + Cutoff Selection Rules + 'CutoffSelectionrule' + Geometrical Selection Rules + 'GeometricalSelectionrule' + Boolean Selection Rules + 'BooleanSelectionrule' + Oriented Selection Sets + 'OrientedSelectionSet.1' + Modeling Groups + 'ModelingGroup.1' + Modeling Plies + 'ModelingPly.1' + Production Plies + 'P1__ModelingPly.1' + Analysis Plies + 'P1L1__ModelingPly.1' + Sensors + 'Sensor' + """ + ) + + +@parametrize_with_cases("model,expected", cases=".", glob="*") +def test_printed_model(model, expected): + """ + Test that model tree looks correct. + """ + tree = get_model_tree(model) - assert ( - os.linesep + str(tree) - == """ -Model - Material Data - Materials - Structural Steel - Fabrics - Fabric.1 - Stackups - Stackup - Sublaminates - SubLaminate - Element Sets - All_Elements - Edge Sets - ns_edge - EdgeSet - Geometry - Cad Geometries - CADGeometry - Virtual Geometries - VirtualGeometry - Rosettes - Global Coordinate System - Lookup Tables - Lookup Tables 1d - LookUpTable1D - Lookup Tables 3d - LookUpTable3D - Selection Rules - Parallel Selection Rules - ParallelSelectionrule - Cylindrical Selection Rules - CylindricalSelectionrule - Tube Selection Rules - TubeSelectionrule - Cutoff Selection Rules - CutoffSelectionrule - Geometrical Selection Rules - GeometricalSelectionrule - Boolean Selection Rules - BooleanSelectionrule - Oriented Selection Sets - OrientedSelectionSet.1 - Modeling Groups - ModelingGroup.1 - ModelingPly.1 - ProductionPly - P1L1__ModelingPly.1 - Sensors - Sensor -""".replace( - "\n", os.linesep - ) - ) + assert str(tree) == expected.replace("\n", os.linesep) From a5dd9dc21f4099791afcef51c99bd6e8b66476c7 Mon Sep 17 00:00:00 2001 From: Dominik Gresch Date: Mon, 18 Nov 2024 17:08:38 +0100 Subject: [PATCH 2/2] Fix doctests, add hide_empty flag to print function --- doc/source/user_guide/howto/print_model.rst | 93 ++++++++++++++++----- src/ansys/acp/core/_model_printer.py | 7 +- 2 files changed, 75 insertions(+), 25 deletions(-) diff --git a/doc/source/user_guide/howto/print_model.rst b/doc/source/user_guide/howto/print_model.rst index 658bd3f7a7..8eaa709f90 100644 --- a/doc/source/user_guide/howto/print_model.rst +++ b/doc/source/user_guide/howto/print_model.rst @@ -21,28 +21,27 @@ You can print the tree structure using the :func:`.print_model` function: .. doctest:: >>> pyacp.print_model(model) - Model - Material Data - Materials - Structural Steel - Fabrics - Fabric.1 + 'ACP Model' + Materials + 'Structural Steel' + Fabrics + 'Fabric.1' Element Sets - All_Elements + 'All_Elements' Edge Sets - ns_edge - Geometry + 'ns_edge' Rosettes - Global Coordinate System - Lookup Tables - Selection Rules + 'Global Coordinate System' Oriented Selection Sets - OrientedSelectionSet.1 + 'OrientedSelectionSet.1' Modeling Groups - ModelingGroup.1 - ModelingPly.1 - ProductionPly - P1L1__ModelingPly.1 + 'ModelingGroup.1' + Modeling Plies + 'ModelingPly.1' + Production Plies + 'P1__ModelingPly.1' + Analysis Plies + 'P1L1__ModelingPly.1' @@ -52,16 +51,66 @@ Alternatively, you can use :func:`.get_model_tree` to get a tree representation. >>> tree_root = pyacp.get_model_tree(model) >>> tree_root.label - 'Model' + "'ACP Model'" >>> for child in tree_root.children: ... print(child.label) ... - Material Data + Materials + Fabrics Element Sets Edge Sets - Geometry Rosettes - Lookup Tables - Selection Rules Oriented Selection Sets Modeling Groups + + +The ``hide_empty`` label can be set to ``False`` to also show empty groups: + +.. doctest:: + + >>> pyacp.print_model(model, hide_empty=False) + 'ACP Model' + Materials + 'Structural Steel' + Fabrics + 'Fabric.1' + Stackups + Sublaminates + Element Sets + 'All_Elements' + Edge Sets + 'ns_edge' + Cad Geometries + Virtual Geometries + Rosettes + 'Global Coordinate System' + Lookup Tables 1d + Lookup Tables 3d + Parallel Selection Rules + Cylindrical Selection Rules + Spherical Selection Rules + Tube Selection Rules + Cutoff Selection Rules + Geometrical Selection Rules + Variable Offset Selection Rules + Boolean Selection Rules + Oriented Selection Sets + 'OrientedSelectionSet.1' + Modeling Groups + 'ModelingGroup.1' + Modeling Plies + 'ModelingPly.1' + Production Plies + 'P1__ModelingPly.1' + Analysis Plies + 'P1L1__ModelingPly.1' + Interface Layers + Butt Joint Sequences + Imported Modeling Groups + Sampling Points + Section Cuts + Solid Models + Imported Solid Models + Sensors + Field Definitions + diff --git a/src/ansys/acp/core/_model_printer.py b/src/ansys/acp/core/_model_printer.py index 307239f691..79fa82f6b1 100644 --- a/src/ansys/acp/core/_model_printer.py +++ b/src/ansys/acp/core/_model_printer.py @@ -54,17 +54,18 @@ def __str__(self, level: int | None = 0) -> str: return ret - -def print_model(model: Model) -> None: +def print_model(model: Model, *, hide_empty: bool = True) -> None: """Print a tree representation of the model. Parameters ---------- model: pyACP model + hide_empty : + Whether to hide empty collections. """ - return print(get_model_tree(model)) + return print(get_model_tree(model, hide_empty=hide_empty)) def get_model_tree(model: Model, *, hide_empty: bool = True) -> Node: