Skip to content

Commit

Permalink
Fix tree printer to include all object types
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
greschd committed Nov 27, 2024
1 parent e36f836 commit 18999ab
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 163 deletions.
106 changes: 34 additions & 72 deletions src/ansys/acp/core/_model_printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand All @@ -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
191 changes: 100 additions & 91 deletions tests/unittests/test_tree_printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)

0 comments on commit 18999ab

Please sign in to comment.