Skip to content

Commit d8b160c

Browse files
jacobrkerstetterpre-commit-ci[bot]pyansys-ci-bot
authored
feat: tessellation speed enhancements (#2294)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <[email protected]>
1 parent 1e85767 commit d8b160c

File tree

9 files changed

+239
-12
lines changed

9 files changed

+239
-12
lines changed

doc/changelog.d/2294.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Tessellation speed enhancements

src/ansys/geometry/core/_grpc/_services/base/designs.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,8 @@ def upload_file(self, **kwargs) -> dict:
9393
def upload_file_stream(self, **kwargs) -> dict:
9494
"""Upload a file to the server using streaming."""
9595
pass
96+
97+
@abstractmethod
98+
def stream_design_tessellation(self, **kwargs) -> dict:
99+
"""Stream the tessellation of a design."""
100+
pass

src/ansys/geometry/core/_grpc/_services/v0/bodies.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from_grpc_material_to_material,
3838
from_grpc_point_to_point3d,
3939
from_grpc_tess_to_pd,
40+
from_grpc_tess_to_raw_data,
4041
from_plane_to_grpc_plane,
4142
from_point3d_to_grpc_point,
4243
from_sketch_shapes_to_grpc_geometries,
@@ -683,7 +684,11 @@ def get_tesellation(self, **kwargs) -> dict: # noqa: D102
683684

684685
for elem in resp:
685686
for face_id, face_tess in elem.face_tessellation.items():
686-
tess_map[face_id] = from_grpc_tess_to_pd(face_tess)
687+
tess_map[face_id] = (
688+
from_grpc_tess_to_raw_data(face_tess)
689+
if kwargs["raw_data"]
690+
else from_grpc_tess_to_pd(face_tess)
691+
)
687692

688693
return {"tessellation": tess_map}
689694

@@ -707,7 +712,11 @@ def get_tesellation_with_options(self, **kwargs) -> dict: # noqa: D102
707712

708713
for elem in resp:
709714
for face_id, face_tess in elem.face_tessellation.items():
710-
tess_map[face_id] = from_grpc_tess_to_pd(face_tess)
715+
tess_map[face_id] = (
716+
from_grpc_tess_to_raw_data(face_tess)
717+
if kwargs["raw_data"]
718+
else from_grpc_tess_to_pd(face_tess)
719+
)
711720

712721
return {"tessellation": tess_map}
713722

src/ansys/geometry/core/_grpc/_services/v0/conversions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,11 @@ def from_grpc_tess_to_pd(tess: GRPCTessellation) -> "pv.PolyData":
375375
return pv.PolyData(var_inp=np.array(tess.vertices).reshape(-1, 3), faces=tess.faces)
376376

377377

378+
def from_grpc_tess_to_raw_data(tess: GRPCTessellation) -> dict:
379+
"""Convert a ``Tessellation`` to raw data."""
380+
return {"vertices": tess.vertices, "faces": tess.faces}
381+
382+
378383
def from_tess_options_to_grpc_tess_options(
379384
options: "TessellationOptions",
380385
) -> GRPCTessellationOptions:

src/ansys/geometry/core/_grpc/_services/v0/designs.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
_check_write_body_facets_input,
3131
build_grpc_id,
3232
from_design_file_format_to_grpc_part_export_format,
33+
from_grpc_tess_to_raw_data,
34+
from_tess_options_to_grpc_tess_options,
3335
)
3436

3537

@@ -256,3 +258,33 @@ def request_generator(
256258

257259
# Return the response - formatted as a dictionary
258260
return {"file_path": response.file_path}
261+
262+
@protect_grpc
263+
def stream_design_tessellation(self, **kwargs) -> dict: # noqa: D102
264+
from ansys.api.dbu.v0.designs_pb2 import DesignTessellationRequest
265+
266+
# If there are options, convert to gRPC options
267+
options = (
268+
from_tess_options_to_grpc_tess_options(kwargs["options"])
269+
if kwargs["options"] is not None
270+
else None
271+
)
272+
273+
# Create the request - assumes all inputs are valid and of the proper type
274+
request = DesignTessellationRequest(options=options)
275+
276+
# Call the gRPC service
277+
response = self.designs_stub.StreamDesignTessellation(request)
278+
279+
# Return the response - formatted as a dictionary
280+
tess_map = {}
281+
for elem in response:
282+
for body_id, body_tess in elem.body_tessellation.items():
283+
tess = {}
284+
for face_id, face_tess in body_tess.face_tessellation.items():
285+
tess[face_id] = from_grpc_tess_to_raw_data(face_tess)
286+
tess_map[body_id] = tess
287+
288+
return {
289+
"tessellation": tess_map,
290+
}

src/ansys/geometry/core/_grpc/_services/v1/designs.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,7 @@ def upload_file(self, **kwargs) -> dict: # noqa: D102
9090
@protect_grpc
9191
def upload_file_stream(self, **kwargs) -> dict: # noqa: D102
9292
raise NotImplementedError
93+
94+
@protect_grpc
95+
def stream_design_tessellation(self, **kwargs) -> dict: # noqa: D102
96+
raise NotImplementedError

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

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -572,9 +572,34 @@ def copy(self, parent: "Component", name: str = None) -> "Body":
572572
"""
573573
return
574574

575+
@abstractmethod
576+
def get_raw_tessellation(
577+
self,
578+
tess_options: TessellationOptions | None = None,
579+
reset_cache: bool = False,
580+
) -> dict:
581+
"""Tessellate the body and return the raw tessellation data.
582+
583+
Parameters
584+
----------
585+
tess_options : TessellationOptions | None, default: None
586+
A set of options to determine the tessellation quality.
587+
reset_cache : bool, default: False
588+
Whether to reset the tessellation cache and re-request the tessellation
589+
from the server.
590+
591+
Returns
592+
-------
593+
dict
594+
Dictionary with face IDs as keys and face tessellation data as values.
595+
"""
596+
575597
@abstractmethod
576598
def tessellate(
577-
self, merge: bool = False, tess_options: TessellationOptions | None = None
599+
self,
600+
merge: bool = False,
601+
tess_options: TessellationOptions | None = None,
602+
reset_cache: bool = False,
578603
) -> Union["PolyData", "MultiBlock"]:
579604
"""Tessellate the body and return the geometry as triangles.
580605
@@ -586,6 +611,9 @@ def tessellate(
586611
When ``True``, the individual faces of the tessellation are merged.
587612
tess_options : TessellationOptions | None, default: None
588613
A set of options to determine the tessellation quality.
614+
reset_cache : bool, default: False
615+
Whether to reset the tessellation cache and re-request the tessellation
616+
from the server.
589617
590618
Returns
591619
-------
@@ -856,6 +884,7 @@ def __init__(
856884
self._surface_offset = None
857885
self._is_alive = True
858886
self._tessellation = None
887+
self._raw_tessellation = None
859888
self._fill_style = FillStyle.DEFAULT
860889
self._color = None
861890

@@ -876,6 +905,7 @@ def reset_tessellation_cache(func): # noqa: N805
876905
@wraps(func)
877906
def wrapper(self: "MasterBody", *args, **kwargs):
878907
self._tessellation = None
908+
self._raw_tessellation = None
879909
return func(self, *args, **kwargs)
880910

881911
return wrapper
@@ -1251,12 +1281,46 @@ def copy(self, parent: "Component", name: str = None) -> "Body": # noqa: D102
12511281
body_id = f"{parent.id}/{tb.id}" if parent.parent_component else tb.id
12521282
return Body(body_id, response.get("name"), parent, tb)
12531283

1284+
def get_raw_tessellation( # noqa: D102
1285+
self,
1286+
tess_options: TessellationOptions | None = None,
1287+
reset_cache: bool = False,
1288+
) -> dict:
1289+
if not self.is_alive:
1290+
return {}
1291+
1292+
# If the server does not support tessellation options, ignore them
1293+
if tess_options is not None and self._grpc_client.backend_version < (25, 2, 0):
1294+
self._grpc_client.log.warning(
1295+
"Tessellation options are not supported by server"
1296+
f" version {self._grpc_client.backend_version}. Ignoring options."
1297+
)
1298+
tess_options = None
1299+
1300+
self._grpc_client.log.debug(f"Requesting tessellation for body {self.id}.")
1301+
1302+
# cache tessellation
1303+
if not self._raw_tessellation or reset_cache:
1304+
if tess_options is not None:
1305+
response = self._grpc_client.services.bodies.get_tesellation_with_options(
1306+
id=self.id, options=tess_options, raw_data=True
1307+
)
1308+
else:
1309+
response = self._grpc_client.services.bodies.get_tesellation(
1310+
id=self.id, backend_version=self._grpc_client.backend_version, raw_data=True
1311+
)
1312+
1313+
self._raw_tessellation = response.get("tessellation")
1314+
1315+
return self._raw_tessellation
1316+
12541317
@graphics_required
12551318
def tessellate( # noqa: D102
12561319
self,
12571320
merge: bool = False,
12581321
transform: Matrix44 = IDENTITY_MATRIX44,
12591322
tess_options: TessellationOptions | None = None,
1323+
reset_cache: bool = False,
12601324
) -> Union["PolyData", "MultiBlock"]:
12611325
# lazy import here to improve initial module load time
12621326
import pyvista as pv
@@ -1275,16 +1339,14 @@ def tessellate( # noqa: D102
12751339
self._grpc_client.log.debug(f"Requesting tessellation for body {self.id}.")
12761340

12771341
# cache tessellation
1278-
if not self._tessellation:
1342+
if not self._tessellation or reset_cache:
12791343
if tess_options is not None:
12801344
response = self._grpc_client.services.bodies.get_tesellation_with_options(
1281-
id=self.id,
1282-
options=tess_options,
1345+
id=self.id, options=tess_options, raw_data=False
12831346
)
12841347
else:
12851348
response = self._grpc_client.services.bodies.get_tesellation(
1286-
id=self.id,
1287-
backend_version=self._grpc_client.backend_version,
1349+
id=self.id, backend_version=self._grpc_client.backend_version, raw_data=False
12881350
)
12891351

12901352
self._tessellation = response.get("tessellation")
@@ -1441,6 +1503,7 @@ def wrapper(self: "Body", *args, **kwargs):
14411503
def _reset_tessellation_cache(self): # noqa: N805
14421504
"""Reset the cached tessellation for a body."""
14431505
self._template._tessellation = None
1506+
self._template._raw_tessellation = None
14441507
# if this reference is stale, reset the real cache in the part
14451508
# this gets the matching id master body in the part
14461509
master_in_part = next(
@@ -1453,6 +1516,7 @@ def _reset_tessellation_cache(self): # noqa: N805
14531516
)
14541517
if master_in_part is not None:
14551518
master_in_part._tessellation = None
1519+
master_in_part._raw_tessellation = None
14561520

14571521
@property
14581522
def id(self) -> str: # noqa: D102
@@ -1786,12 +1850,23 @@ def get_collision(self, body: "Body") -> CollisionType: # noqa: D102
17861850
def copy(self, parent: "Component", name: str = None) -> "Body": # noqa: D102
17871851
return self._template.copy(parent, name)
17881852

1853+
@ensure_design_is_active
1854+
def get_raw_tessellation( # noqa: D102
1855+
self,
1856+
tess_options: TessellationOptions | None = None,
1857+
reset_cache: bool = False,
1858+
) -> dict:
1859+
return self._template.get_raw_tessellation(tess_options, reset_cache)
1860+
17891861
@ensure_design_is_active
17901862
def tessellate( # noqa: D102
1791-
self, merge: bool = False, tess_options: TessellationOptions | None = None
1863+
self,
1864+
merge: bool = False,
1865+
tess_options: TessellationOptions | None = None,
1866+
reset_cache: bool = False,
17921867
) -> Union["PolyData", "MultiBlock"]:
17931868
return self._template.tessellate(
1794-
merge, self.parent_component.get_world_transform(), tess_options
1869+
merge, self.parent_component.get_world_transform(), tess_options, reset_cache
17951870
)
17961871

17971872
@ensure_design_is_active

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

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,12 @@
7171
from ansys.geometry.core.math.plane import Plane
7272
from ansys.geometry.core.math.point import Point3D
7373
from ansys.geometry.core.math.vector import UnitVector3D, Vector3D
74-
from ansys.geometry.core.misc.checks import ensure_design_is_active, min_backend_version
74+
from ansys.geometry.core.misc.checks import (
75+
ensure_design_is_active,
76+
min_backend_version,
77+
)
7578
from ansys.geometry.core.misc.measurements import DEFAULT_UNITS, Distance
76-
from ansys.geometry.core.misc.options import ImportOptions
79+
from ansys.geometry.core.misc.options import ImportOptions, TessellationOptions
7780
from ansys.geometry.core.modeler import Modeler
7881
from ansys.geometry.core.parameters.parameter import Parameter, ParameterUpdateStatus
7982
from ansys.geometry.core.shapes.curves.trimmed_curve import TrimmedCurve
@@ -139,6 +142,7 @@ def __init__(self, name: str, modeler: Modeler, read_existing_design: bool = Fal
139142
self._design_id = ""
140143
self._is_active = False
141144
self._modeler = modeler
145+
self._design_tess = None
142146

143147
# Check whether we want to process an existing design or create a new one.
144148
if read_existing_design:
@@ -1029,6 +1033,44 @@ def insert_file(
10291033
# Return the newly inserted component
10301034
return self._components[-1]
10311035

1036+
@min_backend_version(26, 1, 0)
1037+
@check_input_types
1038+
def get_raw_tessellation(
1039+
self,
1040+
tess_options: TessellationOptions | None = None,
1041+
reset_cache: bool = False,
1042+
) -> dict:
1043+
"""Tessellate the entire design and return the geometry as triangles.
1044+
1045+
Parameters
1046+
----------
1047+
tess_options : TessellationOptions, optional
1048+
Options for the tessellation. If None, default options are used.
1049+
reset_cache : bool, default: False
1050+
Whether to reset the cache before performing the tessellation.
1051+
1052+
Returns
1053+
-------
1054+
dict
1055+
A dictionary with body IDs as keys and another dictionary as values.
1056+
The inner dictionary has face IDs as keys and the corresponding face/vertice arrays
1057+
as values.
1058+
"""
1059+
if not self.is_alive:
1060+
return {} # Return an empty dictionary if the design is not alive
1061+
1062+
self._grpc_client.log.debug(f"Requesting tessellation for design {self.id}.")
1063+
1064+
# cache tessellation
1065+
if not self._design_tess or reset_cache:
1066+
response = self._grpc_client.services.designs.stream_design_tessellation(
1067+
options=tess_options,
1068+
)
1069+
1070+
self._design_tess = response.get("tessellation")
1071+
1072+
return self._design_tess
1073+
10321074
def __repr__(self) -> str:
10331075
"""Represent the ``Design`` as a string."""
10341076
alive_bodies = [1 if body.is_alive else 0 for body in self.bodies]

0 commit comments

Comments
 (0)