Skip to content

Commit 6aac8e5

Browse files
committed
Add a feature tree
Add helper functions to either print a tree of ACP objects, or print it. In the documentation, show the tree structure, and explain how it affects the access and creation of objects. To implement the feature tree, record metadata when creating mapping properties (value type, and read-only status), and use a separate _exposed_grpc_property subclass to indicate it is a mapping.
1 parent 5222628 commit 6aac8e5

File tree

12 files changed

+521
-97
lines changed

12 files changed

+521
-97
lines changed

doc/source/api/example_helpers.rst

-11
This file was deleted.

doc/source/api/extras.rst

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
Extras
2+
------
3+
4+
Example helpers
5+
~~~~~~~~~~~~~~~
6+
7+
.. currentmodule:: ansys.acp.core.extras.example_helpers
8+
9+
.. autosummary::
10+
:toctree: _autosummary
11+
12+
ExampleKeys
13+
get_example_file
14+
set_plot_theme
15+
16+
17+
Feature tree
18+
~~~~~~~~~~~~
19+
20+
.. currentmodule:: ansys.acp.core.extras.feature_tree
21+
22+
..autosummary::
23+
:toctree: _autosummary
24+
25+
get_feature_tree
26+
print_feature_tree

doc/source/api/index.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ API reference
2525
other_utils
2626
dpf_integration_helpers
2727
mechanical_integration_helpers
28-
example_helpers
28+
extras
2929
internal
3030
{% else %}
3131
The API reference is not available in this documentation build.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
Feature tree
2+
------------
3+
4+
.. testsetup::
5+
6+
import ansys.acp.core as pyacp
7+
8+
acp = pyacp.launch_acp()
9+
model = acp.import_model("../tests/data/minimal_complete_model_no_matml_link.acph5")
10+
11+
.. doctest::
12+
13+
>>> pyacp.extras.feature_tree.print_feature_tree()
14+
Model
15+
├── Material
16+
├── Fabric
17+
├── Stackup
18+
├── SubLaminate
19+
├── ElementSet
20+
├── EdgeSet
21+
├── CADGeometry
22+
│ └── CADComponent (read-only)
23+
├── VirtualGeometry
24+
├── Rosette
25+
├── LookUpTable1D
26+
│ └── LookUpTable1DColumn
27+
├── LookUpTable3D
28+
│ └── LookUpTable3DColumn
29+
├── ParallelSelectionRule
30+
├── CylindricalSelectionRule
31+
├── SphericalSelectionRule
32+
├── TubeSelectionRule
33+
├── CutOffSelectionRule
34+
├── GeometricalSelectionRule
35+
├── VariableOffsetSelectionRule
36+
├── BooleanSelectionRule
37+
├── OrientedSelectionSet
38+
├── ModelingGroup
39+
│ ├── ModelingPly
40+
│ │ └── ProductionPly (read-only)
41+
│ │ └── AnalysisPly (read-only)
42+
│ ├── InterfaceLayer
43+
│ └── ButtJointSequence
44+
├── ImportedModelingGroup
45+
│ └── ImportedModelingPly
46+
│ └── ImportedProductionPly (read-only)
47+
│ └── ImportedAnalysisPly (read-only)
48+
├── SamplingPoint
49+
├── SectionCut
50+
├── SolidModel
51+
│ ├── ExtrusionGuide
52+
│ ├── SnapToGeometry
53+
│ ├── SolidElementSet (read-only)
54+
│ ├── CutOffGeometry
55+
│ ├── AnalysisPly (read-only)
56+
│ └── InterfaceLayer (read-only)
57+
├── ImportedSolidModel
58+
│ ├── SolidElementSet (read-only)
59+
│ ├── CutOffGeometry
60+
│ ├── LayupMappingObject
61+
│ │ ├── AnalysisPly (read-only)
62+
│ │ └── ImportedAnalysisPly (read-only)
63+
│ ├── AnalysisPly (read-only)
64+
│ └── ImportedAnalysisPly (read-only)
65+
├── Sensor
66+
└── FieldDefinition
67+
<BLANKLINE>
68+
69+
70+
This structure determines how objects can be created, accessed, and stored in the model.
71+
72+
For example, :class:`ModelingPly` is a child of :class:`ModelingGroup`, which is a child of :class:`Model`.
73+
74+
To access a specific modeling ply, you can traverse this tree hierarchy:
75+
76+
.. doctest::
77+
78+
>>> model
79+
<Model with name 'ACP Model'>
80+
>>> model.modeling_groups
81+
<MutableMapping[ModelingGroup] with keys ['ModelingGroup.1']>
82+
>>> modeling_group = model.modeling_groups["ModelingGroup.1"]
83+
>>> modeling_group.modeling_plies
84+
<MutableMapping[ModelingPly] with keys ['ModelingPly.1']>
85+
>>> modeling_ply = modeling_group.modeling_plies["ModelingPly.1"]
86+
>>> modeling_ply
87+
<ModelingPly with id 'ModelingPly.1'>
88+
89+
To create a new modeling ply, you can use the :meth:`ModelingGroup.create_modeling_ply` method:
90+
91+
.. doctest::
92+
93+
>>> new_ply = modeling_group.create_modeling_ply(name="New Ply")
94+
>>> new_ply
95+
<ModelingPly with id 'New Ply'>
96+
97+
When cloning and storing a modeling ply, the ``parent`` argument must be a :class:`ModelingGroup` object:
98+
99+
.. doctest::
100+
101+
>>> other_modeling_group = model.create_modeling_group()
102+
>>> cloned_ply = modeling_ply.clone()
103+
>>> cloned_ply
104+
<ModelingPly with id ''>
105+
>>> cloned_ply.store(parent=other_modeling_group)
106+
>>> cloned_ply
107+
<ModelingPly with id 'ModelingPly.2'>

doc/source/user_guide/howto/print_model.rst

+63-63
Original file line numberDiff line numberDiff line change
@@ -22,26 +22,26 @@ You can print the tree structure using the :func:`.print_model` function:
2222

2323
>>> pyacp.print_model(model)
2424
'ACP Model'
25-
Materials
26-
'Structural Steel'
27-
Fabrics
28-
'Fabric.1'
29-
Element Sets
30-
'All_Elements'
31-
Edge Sets
32-
'ns_edge'
33-
Rosettes
34-
'Global Coordinate System'
35-
Oriented Selection Sets
36-
'OrientedSelectionSet.1'
37-
Modeling Groups
38-
'ModelingGroup.1'
39-
Modeling Plies
40-
'ModelingPly.1'
41-
Production Plies
42-
'P1__ModelingPly.1'
43-
Analysis Plies
44-
'P1L1__ModelingPly.1'
25+
├── Materials
26+
└── 'Structural Steel'
27+
├── Fabrics
28+
└── 'Fabric.1'
29+
├── Element Sets
30+
└── 'All_Elements'
31+
├── Edge Sets
32+
└── 'ns_edge'
33+
├── Rosettes
34+
└── 'Global Coordinate System'
35+
├── Oriented Selection Sets
36+
└── 'OrientedSelectionSet.1'
37+
└── Modeling Groups
38+
└── 'ModelingGroup.1'
39+
└── Modeling Plies
40+
└── 'ModelingPly.1'
41+
└── Production Plies
42+
└── 'P1__ModelingPly.1'
43+
└── Analysis Plies
44+
└── 'P1L1__ModelingPly.1'
4545
<BLANKLINE>
4646

4747

@@ -70,47 +70,47 @@ The ``hide_empty`` label can be set to ``False`` to also show empty groups:
7070

7171
>>> pyacp.print_model(model, hide_empty=False)
7272
'ACP Model'
73-
Materials
74-
'Structural Steel'
75-
Fabrics
76-
'Fabric.1'
77-
Stackups
78-
Sublaminates
79-
Element Sets
80-
'All_Elements'
81-
Edge Sets
82-
'ns_edge'
83-
Cad Geometries
84-
Virtual Geometries
85-
Rosettes
86-
'Global Coordinate System'
87-
Lookup Tables 1d
88-
Lookup Tables 3d
89-
Parallel Selection Rules
90-
Cylindrical Selection Rules
91-
Spherical Selection Rules
92-
Tube Selection Rules
93-
Cut Off Selection Rules
94-
Geometrical Selection Rules
95-
Variable Offset Selection Rules
96-
Boolean Selection Rules
97-
Oriented Selection Sets
98-
'OrientedSelectionSet.1'
99-
Modeling Groups
100-
'ModelingGroup.1'
101-
Modeling Plies
102-
'ModelingPly.1'
103-
Production Plies
104-
'P1__ModelingPly.1'
105-
Analysis Plies
106-
'P1L1__ModelingPly.1'
107-
Interface Layers
108-
Butt Joint Sequences
109-
Imported Modeling Groups
110-
Sampling Points
111-
Section Cuts
112-
Solid Models
113-
Imported Solid Models
114-
Sensors
115-
Field Definitions
73+
├── Materials
74+
└── 'Structural Steel'
75+
├── Fabrics
76+
└── 'Fabric.1'
77+
├── Stackups
78+
├── Sublaminates
79+
├── Element Sets
80+
└── 'All_Elements'
81+
├── Edge Sets
82+
└── 'ns_edge'
83+
├── Cad Geometries
84+
├── Virtual Geometries
85+
├── Rosettes
86+
└── 'Global Coordinate System'
87+
├── Lookup Tables 1d
88+
├── Lookup Tables 3d
89+
├── Parallel Selection Rules
90+
├── Cylindrical Selection Rules
91+
├── Spherical Selection Rules
92+
├── Tube Selection Rules
93+
├── Cut Off Selection Rules
94+
├── Geometrical Selection Rules
95+
├── Variable Offset Selection Rules
96+
├── Boolean Selection Rules
97+
├── Oriented Selection Sets
98+
└── 'OrientedSelectionSet.1'
99+
├── Modeling Groups
100+
└── 'ModelingGroup.1'
101+
├── Modeling Plies
102+
└── 'ModelingPly.1'
103+
└── Production Plies
104+
└── 'P1__ModelingPly.1'
105+
└── Analysis Plies
106+
└── 'P1L1__ModelingPly.1'
107+
├── Interface Layers
108+
└── Butt Joint Sequences
109+
├── Imported Modeling Groups
110+
├── Sampling Points
111+
├── Section Cuts
112+
├── Solid Models
113+
├── Imported Solid Models
114+
├── Sensors
115+
└── Field Definitions
116116
<BLANKLINE>

src/ansys/acp/core/_model_printer.py

+42-11
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,45 @@ def __init__(self, label: str, children: list["Node"] | None = None):
4545
self.label = label
4646
self.children: list["Node"] = children if children else []
4747

48-
def __str__(self, level: int | None = 0) -> str:
49-
assert level is not None
50-
four_spaces = " "
51-
ret = four_spaces * level + self.label + os.linesep
52-
for child in self.children:
53-
ret += child.__str__(level + 1)
54-
return ret
55-
56-
57-
def print_model(model: Model, *, hide_empty: bool = True) -> None:
48+
def __str__(self) -> str:
49+
return self._to_string(_prefix="", show_lines=True)
50+
51+
def _to_string(
52+
self,
53+
*,
54+
show_lines: bool,
55+
_prefix: str = "",
56+
_is_last_child: bool = True,
57+
_is_root: bool = True,
58+
) -> str:
59+
if show_lines:
60+
elbow = "└── "
61+
line = "│ "
62+
tee = "├── "
63+
empty = " "
64+
else:
65+
elbow = line = tee = empty = " " * 4
66+
if _is_root:
67+
res = _prefix + self.label + os.linesep
68+
else:
69+
res = _prefix + (elbow if _is_last_child else tee) + self.label + os.linesep
70+
for i, child in enumerate(self.children):
71+
if _is_root:
72+
new_prefix = _prefix
73+
elif _is_last_child:
74+
new_prefix = _prefix + empty
75+
else:
76+
new_prefix = _prefix + line
77+
res += child._to_string(
78+
_prefix=new_prefix,
79+
show_lines=show_lines,
80+
_is_last_child=(i == len(self.children) - 1),
81+
_is_root=False,
82+
)
83+
return res
84+
85+
86+
def print_model(model: Model, *, hide_empty: bool = True, show_lines: bool = True) -> None:
5887
"""Print a tree representation of the model.
5988
6089
Parameters
@@ -63,9 +92,11 @@ def print_model(model: Model, *, hide_empty: bool = True) -> None:
6392
pyACP model
6493
hide_empty :
6594
Whether to hide empty collections.
95+
show_lines :
96+
Whether to show lines connecting the nodes.
6697
6798
"""
68-
return print(get_model_tree(model, hide_empty=hide_empty))
99+
return print(get_model_tree(model, hide_empty=hide_empty)._to_string(show_lines=show_lines))
69100

70101

71102
def get_model_tree(model: Model, *, hide_empty: bool = True) -> Node:

src/ansys/acp/core/_tree_objects/_grpc_helpers/mapping.py

+13-3
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
from ..base import CreatableTreeObject, ServerWrapper, TreeObject, TreeObjectBase
4040
from ..enums import Status
4141
from .exceptions import wrap_grpc_errors
42-
from .property_helper import _exposed_grpc_property, _wrap_doc
42+
from .property_helper import _exposed_grpc_mapping_property, _wrap_doc
4343
from .protocols import EditableAndReadableResourceStub, ObjectInfo, ReadableResourceStub
4444

4545
ValueT = TypeVar("ValueT", bound=TreeObjectBase)
@@ -297,7 +297,12 @@ def collection_property(self: ParentT) -> Mapping[ValueT]:
297297
stub=stub_class(channel=self._server_wrapper.channel),
298298
)
299299

300-
return _wrap_doc(_exposed_grpc_property(collection_property), doc=doc)
300+
return _wrap_doc(
301+
_exposed_grpc_mapping_property( # type: ignore
302+
collection_property, value_type=object_class, read_only=True
303+
),
304+
doc=doc,
305+
)
301306

302307

303308
P = ParamSpec("P")
@@ -355,4 +360,9 @@ def collection_property(self: ParentT) -> MutableMapping[CreatableValueT]:
355360
stub=stub_class(channel=self._server_wrapper.channel),
356361
)
357362

358-
return _wrap_doc(_exposed_grpc_property(collection_property), doc=doc)
363+
return _wrap_doc(
364+
_exposed_grpc_mapping_property(
365+
collection_property, value_type=object_class, read_only=False # type: ignore
366+
),
367+
doc=doc,
368+
)

0 commit comments

Comments
 (0)