Skip to content

Commit dc0729a

Browse files
feat: pretty print components (#1403)
Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com>
1 parent 6b6c3ba commit dc0729a

File tree

7 files changed

+469
-0
lines changed

7 files changed

+469
-0
lines changed

doc/changelog.d/1403.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pretty print components
43.8 KB
Loading

doc/source/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ def intersphinx_pyansys_geometry(switcher_version: str):
274274
"examples/03_modeling/sweep_chain_profile": "_static/thumbnails/sweep_chain_profile.png",
275275
"examples/03_modeling/revolving": "_static/thumbnails/revolving.png",
276276
"examples/03_modeling/export_design": "_static/thumbnails/export_design.png",
277+
"examples/03_modeling/design_tree": "_static/thumbnails/design_tree.png",
277278
"examples/04_applied/01_naca_airfoils": "_static/thumbnails/naca_airfoils.png",
278279
"examples/04_applied/02_naca_fluent": "_static/thumbnails/naca_fluent.png",
279280
}

doc/source/examples.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ These examples demonstrate service-based modeling operations.
4242
examples/03_modeling/sweep_chain_profile.mystnb
4343
examples/03_modeling/revolving.mystnb
4444
examples/03_modeling/export_design.mystnb
45+
examples/03_modeling/design_tree.mystnb
4546

4647
Applied examples
4748
----------------
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
---
2+
jupytext:
3+
text_representation:
4+
extension: .mystnb
5+
format_name: myst
6+
format_version: 0.13
7+
jupytext_version: 1.14.1
8+
kernelspec:
9+
display_name: Python 3 (ipykernel)
10+
language: python
11+
name: python3
12+
---
13+
# Modeling: Visualization of the design tree on terminal
14+
15+
A user can visualize its model object tree easily by using the ``tree_print()`` method
16+
available on the ``Design`` and ``Component`` objects. This method prints the tree
17+
structure of the model in the terminal.
18+
19+
## Perform required imports
20+
21+
For the following example, we need to import these modules:
22+
23+
```{code-cell} ipython3
24+
from pint import Quantity
25+
26+
from ansys.geometry.core import Modeler
27+
from ansys.geometry.core.math.constants import UNITVECTOR3D_X, UNITVECTOR3D_Y
28+
from ansys.geometry.core.math.point import Point2D, Point3D
29+
from ansys.geometry.core.misc.units import UNITS
30+
from ansys.geometry.core.sketch.sketch import Sketch
31+
```
32+
33+
## Create a design
34+
35+
The following code creates a simple design for demonstration purposes. The design consists of
36+
several cylinders extruded. The interesting part is visualizing the corresponding design tree.
37+
38+
```{code-cell} ipython3
39+
# Create a modeler object
40+
modeler = Modeler()
41+
42+
# Create your design on the server side
43+
design = modeler.create_design("TreePrintComponent")
44+
45+
# Create a Sketch object and draw a circle (all client side)
46+
sketch = Sketch()
47+
sketch.circle(Point2D([-30, -30]), 10 * UNITS.m)
48+
distance = 30 * UNITS.m
49+
50+
# The following component hierarchy is made
51+
#
52+
# |---> comp_1 ---|---> nested_1_comp_1 ---> nested_1_nested_1_comp_1
53+
# | |
54+
# | |---> nested_2_comp_1
55+
# |
56+
# DESIGN ---|---> comp_2 -------> nested_1_comp_2
57+
# |
58+
# |
59+
# |---> comp_3
60+
#
61+
#
62+
# Now, only "comp_3", "nested_2_comp_1" and "nested_1_nested_1_comp_1"
63+
# will have a body associated.
64+
#
65+
66+
# Create the components
67+
comp_1 = design.add_component("Component_1")
68+
comp_2 = design.add_component("Component_2")
69+
comp_3 = design.add_component("Component_3")
70+
nested_1_comp_1 = comp_1.add_component("Nested_1_Component_1")
71+
nested_1_nested_1_comp_1 = nested_1_comp_1.add_component("Nested_1_Nested_1_Component_1")
72+
nested_2_comp_1 = comp_1.add_component("Nested_2_Component_1")
73+
nested_1_comp_2 = comp_2.add_component("Nested_1_Component_2")
74+
75+
# Create the bodies
76+
b1 = comp_3.extrude_sketch(name="comp_3_circle", sketch=sketch, distance=distance)
77+
b2 = nested_2_comp_1.extrude_sketch(
78+
name="nested_2_comp_1_circle", sketch=sketch, distance=distance
79+
)
80+
b2.translate(UNITVECTOR3D_X, 50)
81+
b3 = nested_1_nested_1_comp_1.extrude_sketch(
82+
name="nested_1_nested_1_comp_1_circle", sketch=sketch, distance=distance
83+
)
84+
b3.translate(UNITVECTOR3D_Y, 50)
85+
86+
# Create beams (in design)
87+
circle_profile_1 = design.add_beam_circular_profile(
88+
"CircleProfile1", Quantity(10, UNITS.mm), Point3D([0, 0, 0]), UNITVECTOR3D_X, UNITVECTOR3D_Y
89+
)
90+
beam_1 = nested_1_comp_2.create_beam(
91+
Point3D([9, 99, 999], UNITS.mm), Point3D([8, 88, 888], UNITS.mm), circle_profile_1
92+
)
93+
94+
design.plot()
95+
```
96+
97+
## Visualize the design tree
98+
99+
Now, let's visualize the design tree using the ``tree_print()`` method. Let's start by
100+
printing the tree structure of the design object with no extra arguments.
101+
102+
```{code-cell} ipython3
103+
design.tree_print()
104+
```
105+
106+
### Controlling the depth of the tree
107+
108+
The ``tree_print()`` method accepts an optional argument ``depth`` to control the depth of the
109+
tree to be printed. The default value is ``None``, which means the entire tree will be printed.
110+
111+
```{code-cell} ipython3
112+
design.tree_print(depth=1)
113+
```
114+
115+
In this case, only the first level of the tree is printed - that is, the three main
116+
components.
117+
118+
### Excluding bodies, components, or beams
119+
120+
By default, the ``tree_print()`` method prints all the bodies, components, and beams in the
121+
design tree. However, you can exclude any of these by setting the corresponding argument to
122+
``False``.
123+
124+
```{code-cell} ipython3
125+
design.tree_print(consider_bodies=False, consider_beams=False)
126+
```
127+
128+
In this case, the bodies and beams are not be printed in the tree structure.
129+
130+
```{code-cell} ipython3
131+
design.tree_print(consider_comps=False)
132+
```
133+
134+
In this case, the components are not be printed in the tree structure - leaving only the
135+
design object represented.
136+
137+
### Sorting the tree
138+
139+
By default, the tree structure is sorted by the way the components, bodies, and beams were
140+
created. However, you can sort the tree structure by setting the ``sort_keys`` argument to ``True``.
141+
In that case, the tree is sorted alphabetically.
142+
143+
Let's add a new component to the design and print the tree structure by default.
144+
145+
```{code-cell} ipython3
146+
comp_4 = design.add_component("A_Component")
147+
design.tree_print(depth=1)
148+
```
149+
150+
Now, let's print the tree structure with the components sorted alphabetically.
151+
152+
```{code-cell} ipython3
153+
design.tree_print(depth=1, sort_keys=True)
154+
```
155+
156+
### Indenting the tree
157+
158+
By default, the tree structure is printed with an indentation level of 4. However, you
159+
can indent the tree structure by setting the ``indent`` argument to the desired value.
160+
161+
```{code-cell} ipython3
162+
design.tree_print(depth=1, indent=8)
163+
```
164+
165+
In this case, the tree structure is printed with an indentation level of 8.
166+
167+
### Printing the tree from a specific component
168+
169+
You can print the tree structure from a specific component by calling the ``tree_print()``
170+
method on the component object.
171+
172+
```{code-cell} ipython3
173+
nested_1_comp_1.tree_print()
174+
```

src/ansys/geometry/core/designer/component.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1472,3 +1472,143 @@ def __repr__(self) -> str:
14721472
lines.append(f" N Design Points : {len(self.design_points)}")
14731473
lines.append(f" N Components : {sum(alive_comps)}")
14741474
return "\n".join(lines)
1475+
1476+
@check_input_types
1477+
def tree_print(
1478+
self,
1479+
consider_comps: bool = True,
1480+
consider_bodies: bool = True,
1481+
consider_beams: bool = True,
1482+
depth: int | None = None,
1483+
indent: int = 4,
1484+
sort_keys: bool = False,
1485+
return_list: bool = False,
1486+
skip_loc_header: bool = False,
1487+
) -> None | list[str]:
1488+
"""Print the component in tree format.
1489+
1490+
Parameters
1491+
----------
1492+
consider_comps : bool, default: True
1493+
Whether to print the nested components.
1494+
consider_bodies : bool, default: True
1495+
Whether to print the bodies.
1496+
consider_beams : bool, default: True
1497+
Whether to print the beams.
1498+
depth : int | None, default: None
1499+
Depth level to print. If None, it prints all levels.
1500+
indent : int, default: 4
1501+
Indentation level. Minimum is 2 - if less than 2, it is set to 2
1502+
by default.
1503+
sort_keys : bool, default: False
1504+
Whether to sort the keys alphabetically.
1505+
return_list : bool, default: False
1506+
Whether to return a list of strings or print out
1507+
the tree structure.
1508+
skip_loc_header : bool, default: False
1509+
Whether to skip the location header. Mostly for internal use.
1510+
1511+
Returns
1512+
-------
1513+
None | list[str]
1514+
Tree-style printed component or list of strings representing the component tree.
1515+
"""
1516+
1517+
def build_parent_tree(comp: Component, parent_tree: str = "") -> str:
1518+
"""Private function to build the parent tree of a component."""
1519+
if comp.parent_component is None:
1520+
# We reached the top level component... return the parent tree
1521+
return "Root component (Design)" if not parent_tree else parent_tree
1522+
else:
1523+
if parent_tree == "":
1524+
# Should only happen in the first call
1525+
parent_tree = comp.name
1526+
1527+
# Add the parent component to the parent tree and continue
1528+
return build_parent_tree(
1529+
comp.parent_component, f"{comp.parent_component.name} > {parent_tree}"
1530+
)
1531+
1532+
# Indentation should be at least 2
1533+
indent = max(2, indent)
1534+
1535+
# Initialize the lines list
1536+
lines: list[str] = []
1537+
1538+
# Add the location header if requested - and only on the first call
1539+
# (subsequent calls will have the skip_loc_header set to True)
1540+
if not skip_loc_header:
1541+
lines.append(f">>> Tree print view of component '{self.name}'")
1542+
lines.append("")
1543+
lines.append("Location")
1544+
lines.append(f"{'-' * len(lines[-1])}")
1545+
lines.append(f"{build_parent_tree(self)}")
1546+
lines.append("")
1547+
lines.append("Subtree")
1548+
lines.append(f"{'-' * len(lines[-1])}")
1549+
1550+
lines.append(f"(comp) {self.name}")
1551+
# Print the bodies
1552+
if consider_bodies:
1553+
# Check if the bodies should be sorted
1554+
if sort_keys:
1555+
body_names = [body.name for body in sorted(self.bodies, key=lambda body: body.name)]
1556+
else:
1557+
body_names = [body.name for body in self.bodies]
1558+
1559+
# Add the bodies to the lines (with indentation)
1560+
lines.extend([f"|{'-' * (indent-1)}(body) {name}" for name in body_names])
1561+
1562+
# Print the beams
1563+
if consider_beams:
1564+
# Check if the bodies should be sorted
1565+
if sort_keys:
1566+
# TODO: Beams should also have names...
1567+
# https://github.com/ansys/pyansys-geometry/issues/1319
1568+
beam_names = [
1569+
beam.id
1570+
for beam in sorted(self.beams, key=lambda beam: beam.id)
1571+
if beam.is_alive
1572+
]
1573+
else:
1574+
beam_names = [beam.id for beam in self.beams if beam.is_alive]
1575+
1576+
# Add the bodies to the lines (with indentation)
1577+
lines.extend([f"|{'-' * (indent-1)}(beam) {name}" for name in beam_names])
1578+
1579+
# Print the nested components
1580+
if consider_comps:
1581+
# Check if the components should be sorted
1582+
comps = (
1583+
self.components
1584+
if not sort_keys
1585+
else sorted(self.components, key=lambda comp: comp.name)
1586+
)
1587+
comps = [comp for comp in comps if comp.is_alive]
1588+
1589+
# Add the components to the lines (recursive)
1590+
if depth is None or depth > 1:
1591+
n_comps = len(comps)
1592+
for idx, comp in enumerate(comps):
1593+
subcomp = comp.tree_print(
1594+
consider_comps=consider_comps,
1595+
consider_bodies=consider_bodies,
1596+
consider_beams=consider_beams,
1597+
depth=None if depth is None else depth - 1,
1598+
indent=indent,
1599+
sort_keys=sort_keys,
1600+
return_list=True,
1601+
skip_loc_header=True,
1602+
)
1603+
1604+
# Add indentation to the subcomponent lines
1605+
lines.append(f"|{'-' * (indent-1)}(comp) {comp.name}")
1606+
1607+
# Determine the prefix for the subcomponent lines and add them
1608+
prefix = f"{' ' * indent}" if idx == (n_comps - 1) else f":{' ' * (indent-1)}"
1609+
lines.extend([f"{prefix}{line}" for line in subcomp[1:]])
1610+
1611+
else:
1612+
lines.extend([f"|{'-' * (indent-1)}(comp) {comp.name}" for comp in comps])
1613+
1614+
return lines if return_list else print("\n".join(lines))

0 commit comments

Comments
 (0)