From b70e431b131ac4c0d9d685412172a55077657e16 Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Mon, 21 Oct 2024 14:28:34 +0200 Subject: [PATCH] feat: performance enhancements to plotter (#1496) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- doc/changelog.d/1496.added.md | 1 + doc/source/_static/assets/multicolors.png | Bin 60732 -> 0 bytes .../05_plotter_picker.mystnb | 4 +- .../03_modeling/tessellation_usage.mystnb | 16 +-- pyproject.toml | 4 +- .../geometry/core/connection/conversions.py | 2 +- src/ansys/geometry/core/designer/body.py | 24 ++-- src/ansys/geometry/core/designer/component.py | 115 +++++++----------- src/ansys/geometry/core/plotting/plotter.py | 56 +++++---- tests/integration/test_plotter.py | 28 +++++ tests/integration/test_tessellation.py | 27 +--- 12 files changed, 132 insertions(+), 147 deletions(-) create mode 100644 doc/changelog.d/1496.added.md delete mode 100644 doc/source/_static/assets/multicolors.png diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 387a0fc0c2..62a39668c2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ exclude: "tests/integration/files" repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.9 + rev: v0.7.0 hooks: - id: ruff - id: ruff-format diff --git a/doc/changelog.d/1496.added.md b/doc/changelog.d/1496.added.md new file mode 100644 index 0000000000..fd782cbc1c --- /dev/null +++ b/doc/changelog.d/1496.added.md @@ -0,0 +1 @@ +performance enhancements to plotter \ No newline at end of file diff --git a/doc/source/_static/assets/multicolors.png b/doc/source/_static/assets/multicolors.png deleted file mode 100644 index 65f8484ffc56932d38f0fcbd1a148d8d2ea09b7a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60732 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 bb}xw8yT(zv0uVhS;Pp`bv09<3MezRsMR>!V diff --git a/doc/source/examples/01_getting_started/05_plotter_picker.mystnb b/doc/source/examples/01_getting_started/05_plotter_picker.mystnb index f0faf977fb..5a65235af9 100644 --- a/doc/source/examples/01_getting_started/05_plotter_picker.mystnb +++ b/doc/source/examples/01_getting_started/05_plotter_picker.mystnb @@ -176,7 +176,7 @@ the ``plot()`` method. In the following cell we will create a new design and plot a prism and a cylinder in different colors. -```python +```{code-cell} ipython design = modeler.create_design("MultiColors") # Create a sketch of a box @@ -193,8 +193,6 @@ design.extrude_sketch("Cylinder", sketch_circle, 50 * UNITS.m) design.plot(multi_colors=True) ``` -![](../../_static/assets/multicolors.png) - ## Clip objects You can clip any object represented in the plotter by defining a ``Plane`` object that diff --git a/doc/source/examples/03_modeling/tessellation_usage.mystnb b/doc/source/examples/03_modeling/tessellation_usage.mystnb index b606cc2ea8..3afbb3cadf 100644 --- a/doc/source/examples/03_modeling/tessellation_usage.mystnb +++ b/doc/source/examples/03_modeling/tessellation_usage.mystnb @@ -76,26 +76,18 @@ combines all the faces of each individual body into a single dataset without separating faces. ```{code-cell} ipython3 -dataset = comp.tessellate(merge_bodies=True) +dataset = comp.tessellate() dataset ``` -If you want to tessellate the body and return the geometry as triangles, single body tessellation -is possible. If you want to merge the individual faces of the tessellation, enable the -``merge`` option so that the body is rendered into a single mesh. This preserves the number of -triangles and only merges the topology. -**Code without merging the body** +Single body tessellation is possible. In that case, users can request the body-level tessellation +method to tessellate the body and merge all the faces into a single dataset. ```{code-cell} ipython3 -dataset = body.tessellate() +dataset = comp.bodies[0].tessellate() dataset ``` -**Code with merging the body** -```{code-cell} ipython3 -mesh = body.tessellate(merge=True) -mesh -``` ## Plot design Plot the design. diff --git a/pyproject.toml b/pyproject.toml index 489992b822..da2aaf1139 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ all = [ tests = [ "ansys-platform-instancemanagement==1.1.2", "ansys-tools-path==0.6.0", - "ansys-tools-visualization-interface==0.4.5", + "ansys-tools-visualization-interface==0.4.6", "beartype==0.19.0", "docker==7.1.0", "grpcio==1.67.0", @@ -77,7 +77,7 @@ tests-minimal = [ doc = [ "ansys-sphinx-theme[autoapi]==1.1.6", "ansys-tools-path==0.6.0", - "ansys-tools-visualization-interface==0.4.5", + "ansys-tools-visualization-interface==0.4.6", "beartype==0.19.0", "docker==7.1.0", "grpcio==1.67.0", diff --git a/src/ansys/geometry/core/connection/conversions.py b/src/ansys/geometry/core/connection/conversions.py index 7b77da3e78..8804b21438 100644 --- a/src/ansys/geometry/core/connection/conversions.py +++ b/src/ansys/geometry/core/connection/conversions.py @@ -382,7 +382,7 @@ def tess_to_pd(tess: Tessellation) -> "PolyData": import numpy as np import pyvista as pv - return pv.PolyData(np.array(tess.vertices).reshape(-1, 3), tess.faces) + return pv.PolyData(var_inp=np.array(tess.vertices).reshape(-1, 3), faces=tess.faces) def grpc_matrix_to_matrix(m: GRPCMatrix) -> Matrix44: diff --git a/src/ansys/geometry/core/designer/body.py b/src/ansys/geometry/core/designer/body.py index 0db35225c5..cb2d031e91 100644 --- a/src/ansys/geometry/core/designer/body.py +++ b/src/ansys/geometry/core/designer/body.py @@ -544,7 +544,7 @@ def tessellate(self, merge: bool = False) -> Union["PolyData", "MultiBlock"]: @abstractmethod def plot( self, - merge: bool = False, + merge: bool = True, screenshot: str | None = None, use_trame: bool | None = None, use_service_colors: bool | None = None, @@ -554,10 +554,11 @@ def plot( Parameters ---------- - merge : bool, default: False - Whether to merge the body into a single mesh. When ``False`` (default), - the number of triangles are preserved and only the topology is merged. - When ``True``, the individual faces of the tessellation are merged. + merge : bool, default: True + Whether to merge the body into a single mesh. Performance improved when ``True``. + When ``True`` (default), the individual faces of the tessellation are merged. + When ``False``, the number of triangles are preserved and only the topology + is merged. screenshot : str, default: None Path for saving a screenshot of the image that is being represented. use_trame : bool, default: None @@ -1118,14 +1119,16 @@ def tessellate( # noqa: D102 pdata = [tess_to_pd(tess).transform(transform) for tess in self._tessellation] comp = pv.MultiBlock(pdata) + if merge: ugrid = comp.combine() - return pv.PolyData(ugrid.points, ugrid.cells, n_faces=ugrid.n_cells) - return comp + return pv.PolyData(var_inp=ugrid.points, faces=ugrid.cells) + else: + return comp def plot( # noqa: D102 self, - merge: bool = False, + merge: bool = True, screenshot: str | None = None, use_trame: bool | None = None, use_service_colors: bool | None = None, @@ -1510,7 +1513,7 @@ def tessellate( # noqa: D102 def plot( # noqa: D102 self, - merge: bool = False, + merge: bool = True, screenshot: str | None = None, use_trame: bool | None = None, use_service_colors: bool | None = None, @@ -1529,6 +1532,9 @@ def plot( # noqa: D102 else pyansys_geometry.USE_SERVICE_COLORS ) + # Add to plotting options as well... to be used by the plotter if necessary + plotting_options["merge_bodies"] = merge + mesh_object = ( self if use_service_colors else MeshObjectPlot(self, self.tessellate(merge=merge)) ) diff --git a/src/ansys/geometry/core/designer/component.py b/src/ansys/geometry/core/designer/component.py index bef18fd6b0..1b3d52bea8 100644 --- a/src/ansys/geometry/core/designer/component.py +++ b/src/ansys/geometry/core/designer/component.py @@ -1350,85 +1350,47 @@ def _kill_component_on_client(self) -> None: # Kill itself self._is_alive = False - def tessellate( - self, merge_component: bool = False, merge_bodies: bool = False - ) -> Union["PolyData", "MultiBlock"]: + def tessellate(self, _recursive_call: bool = False) -> Union["PolyData", list["MultiBlock"]]: """Tessellate the component. Parameters ---------- - merge_component : bool, default: False - Whether to merge this component into a single dataset. When ``True``, - all the individual bodies are effectively combined into a single - dataset without any hierarchy. - merge_bodies : bool, default: False - Whether to merge each body into a single dataset. When ``True``, - all the faces of each individual body are effectively - merged into a single dataset without separating faces. + _recursive_call: bool, default: False + Internal flag to indicate if this method is being called recursively. + Not to be used by the user. Returns ------- - ~pyvista.PolyData, ~pyvista.MultiBlock - Merged :class:`pyvista.PolyData` if ``merge_component=True`` or a - composite dataset. + ~pyvista.PolyData, list[~pyvista.MultiBlock] + Tessellated component as a single PolyData object. + If the method is called recursively, a list of MultiBlock objects is returned. - Examples - -------- - Create two stacked bodies and return the tessellation as two merged bodies: - - >>> from ansys.geometry.core.sketch import Sketch - >>> from ansys.geometry.core import Modeler - >>> from ansys.geometry.core.math import Point2D, Point3D, Plane - >>> from ansys.geometry.core.misc import UNITS - >>> modeler = Modeler("10.54.0.72", "50051") - >>> sketch_1 = Sketch() - >>> box = sketch_1.box( - >>> Point2D([10, 10], UNITS.m), Quantity(10, UNITS.m), Quantity(5, UNITS.m)) - >>> sketch_1.circle(Point2D([0, 0], UNITS.m), Quantity(25, UNITS.m)) - >>> design = modeler.create_design("MyDesign") - >>> comp = design.add_component("MyComponent") - >>> distance = Quantity(10, UNITS.m) - >>> body = comp.extrude_sketch("Body", sketch=sketch_1, distance=distance) - >>> sketch_2 = Sketch(Plane([0, 0, 10])) - >>> box = sketch_2.box( - >>> Point2D([10, 10], UNITS.m), Quantity(10, UNITS.m), Quantity(5, UNITS.m)) - >>> circle = sketch_2.circle(Point2D([0, 0], UNITS.m), Quantity(25, UNITS.m)) - >>> body = comp.extrude_sketch("Body", sketch=sketch_2, distance=distance) - >>> dataset = comp.tessellate(merge_bodies=True) - >>> dataset - MultiBlock (0x7ff6bcb511e0) - N Blocks: 2 - X Bounds: -25.000, 25.000 - Y Bounds: -24.991, 24.991 - Z Bounds: 0.000, 20.000 """ import pyvista as pv # Tessellate the bodies in this component - datasets = [body.tessellate(merge_bodies) for body in self.bodies] - - blocks_list = [pv.MultiBlock(datasets)] + datasets: list["MultiBlock"] = [body.tessellate(merge=False) for body in self.bodies] # Now, go recursively inside its subcomponents (with no arguments) and # merge the PolyData obtained into our blocks for comp in self._components: if not comp.is_alive: continue - blocks_list.append(comp.tessellate(merge_bodies=merge_bodies)) - - # Transform the list of MultiBlock objects into a single MultiBlock - blocks = pv.MultiBlock(blocks_list) + datasets.extend(comp.tessellate(_recursive_call=True)) - if merge_component: - ugrid = blocks.combine() - # Convert to polydata as it's slightly faster than extract surface - return pv.PolyData(ugrid.points, ugrid.cells, n_faces=ugrid.n_cells) - return blocks + # Convert to polydata as it's slightly faster than extract surface + # plus this method is only for visualizing the component as a whole (no + # need to keep the hierarchy) + if _recursive_call: + return datasets + else: + ugrid = pv.MultiBlock(datasets).combine() + return pv.PolyData(var_inp=ugrid.points, faces=ugrid.cells) def plot( self, - merge_component: bool = False, - merge_bodies: bool = False, + merge_component: bool = True, + merge_bodies: bool = True, screenshot: str | None = None, use_trame: bool | None = None, use_service_colors: bool | None = None, @@ -1438,14 +1400,15 @@ def plot( Parameters ---------- - merge_component : bool, default: False - Whether to merge the component into a single dataset. When ``True``, - all the individual bodies are effectively merged into a single - dataset without any hierarchy. - merge_bodies : bool, default: False - Whether to merge each body into a single dataset. When ``True``, - all the faces of each individual body are effectively merged - into a single dataset without separating faces. + merge_component : bool, default: True + Whether to merge the component into a single dataset. By default, ``True``. + Performance improved. When ``True``, all the faces of the component are effectively + merged into a single dataset. If ``False``, the individual bodies are kept separate. + merge_bodies : bool, default: True + Whether to merge each body into a single dataset. By default, ``True``. + Performance improved. When ``True``, all the faces of each individual body are + effectively merged into a single dataset. If ``False``, the individual faces are kept + separate. screenshot : str, default: None Path for saving a screenshot of the image being represented. use_trame : bool, default: None @@ -1496,7 +1459,6 @@ def plot( """ import ansys.geometry.core as pyansys_geometry from ansys.geometry.core.plotting import GeometryPlotter - from ansys.tools.visualization_interface.types.mesh_object_plot import MeshObjectPlot use_service_colors = ( use_service_colors @@ -1504,16 +1466,21 @@ def plot( else pyansys_geometry.USE_SERVICE_COLORS ) - mesh_object = ( - self - if use_service_colors - else MeshObjectPlot( - custom_object=self, - mesh=self.tessellate(merge_component=merge_component, merge_bodies=merge_bodies), + # Add merge_component and merge_bodies to the plotting options + plotting_options["merge_component"] = merge_component + plotting_options["merge_bodies"] = merge_bodies + + # At component level, if ``multi_colors`` or ``use_service_colors`` are defined + # we should not merge the component. + if plotting_options.get("multi_colors", False) or use_service_colors: + plotting_options["merge_component"] = False + self._grpc_client.log.info( + "Ignoring 'merge_component=True' (default behavior) as " + "'multi_colors' or 'use_service_colors' are defined." ) - ) + pl = GeometryPlotter(use_trame=use_trame, use_service_colors=use_service_colors) - pl.plot(mesh_object, **plotting_options) + pl.plot(self, **plotting_options) pl.show(screenshot=screenshot, **plotting_options) def __repr__(self) -> str: diff --git a/src/ansys/geometry/core/plotting/plotter.py b/src/ansys/geometry/core/plotting/plotter.py index bbe0cfe7fe..f883f9dbd3 100644 --- a/src/ansys/geometry/core/plotting/plotter.py +++ b/src/ansys/geometry/core/plotting/plotter.py @@ -21,6 +21,7 @@ # SOFTWARE. """Provides plotting for various PyAnsys Geometry objects.""" +from itertools import cycle from typing import Any import numpy as np @@ -45,6 +46,8 @@ ) from ansys.tools.visualization_interface.backends.pyvista import PyVistaBackend +POLYDATA_COLOR_CYCLER = cycle(pv.colors.get_cycler("matplotlib")) + class GeometryPlotter(PlotterInterface): """Plotter for PyAnsys Geometry objects. @@ -235,13 +238,25 @@ def add_body(self, body: Body, merge: bool = False, **plotting_options: dict | N """ if self.use_service_colors: plotting_options["color"] = body.color + # WORKAROUND: multi_colors is not properly supported in PyVista PolyData + # so if multi_colors is True and merge is True (returns PolyData) then + # we need to set the color manually + elif ( + merge + and plotting_options.get("multi_colors", False) + and "color" not in plotting_options + ): + plotting_options["color"] = next(POLYDATA_COLOR_CYCLER)["color"] # Use the default PyAnsys Geometry add_mesh arguments self._backend.pv_interface.set_add_mesh_defaults(plotting_options) dataset = body.tessellate(merge=merge) body_plot = MeshObjectPlot(custom_object=body, mesh=dataset) self._backend.pv_interface.plot(body_plot, **plotting_options) - self.add_body_edges(body_plot) + + # Edges should ONLY be plotted individually if picking is enabled + if self._backend._allow_picking: + self.add_body_edges(body_plot) def add_component( self, @@ -268,24 +283,21 @@ def add_component( Keyword arguments. For allowable keyword arguments, see the :meth:`Plotter.add_mesh ` method. """ - if self.use_service_colors: - # We need to iterate over the bodies and subcomponents to set the color... - # this leads to a different logic for setting the color - LOG.warning("Using service colors for plotting a component") - LOG.warning(">>> Iterating over the bodies and subcomponents to set the color...") - LOG.warning(">>> Ignoring values for merge_component and merge_bodies.") - LOG.warning(">>> This will be slow for large components.") - self.add_component_by_body(component, **plotting_options) - else: - # Use the default PyAnsys Geometry add_mesh arguments + if merge_component: self._backend.pv_interface.set_add_mesh_defaults(plotting_options) - dataset = component.tessellate( - merge_component=merge_component, merge_bodies=merge_bodies - ) + dataset = component.tessellate() component_polydata = MeshObjectPlot(component, dataset) self.plot(component_polydata, **plotting_options) + else: + self.add_component_by_body( + component, + merge_bodies=merge_bodies, + **plotting_options, + ) - def add_component_by_body(self, component: Component, **plotting_options: dict | None) -> None: + def add_component_by_body( + self, component: Component, merge_bodies: bool, **plotting_options: dict | None + ) -> None: """Add a component on a per body basis. Notes @@ -303,9 +315,9 @@ def add_component_by_body(self, component: Component, **plotting_options: dict | """ # Recursively add the bodies and components for body in component.bodies: - self.add_body(body, **plotting_options) + self.add_body(body, merge=merge_bodies, **plotting_options) for comp in component.components: - self.add_component_by_body(comp, **plotting_options) + self.add_component_by_body(comp, merge_bodies=merge_bodies, **plotting_options) def add_sketch_polydata( self, polydata_entries: list[pv.PolyData], sketch: Sketch = None, **plotting_options @@ -391,11 +403,11 @@ def plot(self, plottable_object: Any, name_filter: str = None, **plotting_option else: merge_bodies = None - if "merge_components" in plotting_options: - merge_components = plotting_options["merge_components"] - plotting_options.pop("merge_components", None) + if "merge_component" in plotting_options: + merge_component = plotting_options["merge_component"] + plotting_options.pop("merge_component", None) else: - merge_components = None + merge_component = None # Add the custom object to the plotter if isinstance(plottable_object, DesignPoint): self.add_design_point(plottable_object, **plotting_options) @@ -404,7 +416,7 @@ def plot(self, plottable_object: Any, name_filter: str = None, **plotting_option elif isinstance(plottable_object, (Body, MasterBody)): self.add_body(plottable_object, merge_bodies, **plotting_options) elif isinstance(plottable_object, (Design, Component)): - self.add_component(plottable_object, merge_components, merge_bodies, **plotting_options) + self.add_component(plottable_object, merge_component, merge_bodies, **plotting_options) elif ( isinstance(plottable_object, list) and len(plottable_object) > 0 diff --git a/tests/integration/test_plotter.py b/tests/integration/test_plotter.py index 41367b575c..1a5c67a379 100644 --- a/tests/integration/test_plotter.py +++ b/tests/integration/test_plotter.py @@ -875,3 +875,31 @@ def test_plot_server_color_on_single_body_using_input(modeler: Modeler, verify_i screenshot=Path(IMAGE_RESULTS_DIR, "plot_server_color_on_single_body_using_input.png"), use_service_colors=True, ) + + +@skip_no_xserver +def test_plot_design_multi_colors(modeler: Modeler, verify_image_cache): + """Test plotting of design with/without multi_colors.""" + design = modeler.create_design("DesignMultiColors") + # Create a sketch of a box + sketch_box = Sketch().box( + Point2D([0, 0], unit=UNITS.m), width=30 * UNITS.m, height=40 * UNITS.m + ) + + # Create a sketch of a circle (overlapping the box slightly) + sketch_circle = Sketch().circle(Point2D([20, 0], unit=UNITS.m), radius=3 * UNITS.m) + + # Extrude both sketches to get a prism and a cylinder + design.extrude_sketch("Prism", sketch_box, 50 * UNITS.m) + design.extrude_sketch("Cylinder", sketch_circle, 50 * UNITS.m) + + # Design plotting + design.plot( + screenshot=Path(IMAGE_RESULTS_DIR, "plot_design_multi_colors.png"), + multi_colors=True, + ) + + design.plot( + screenshot=Path(IMAGE_RESULTS_DIR, "plot_design_multi_colors_single_color.png"), + multi_colors=False, + ) diff --git a/tests/integration/test_tessellation.py b/tests/integration/test_tessellation.py index 49d97c6143..3f75421dcf 100644 --- a/tests/integration/test_tessellation.py +++ b/tests/integration/test_tessellation.py @@ -131,27 +131,8 @@ def test_component_tessellate(modeler: Modeler): sketch_2.circle(Point2D([0, 0], UNITS.m), Quantity(25, UNITS.m)) comp.extrude_sketch("Body", sketch=sketch_2, distance=distance) - # Tessellate the component by merging all the faces of each individual body - # and creates a single dataset - dataset = comp.tessellate(merge_bodies=True) - assert "MultiBlock" in str(dataset) - assert dataset.n_blocks == 1 - assert dataset.center == pytest.approx([0.0, 0.0, 10.0], rel=1e-6, abs=1e-8) - if modeler.client.backend_type != BackendType.LINUX_SERVICE: - assert dataset.bounds == pytest.approx( - [-25.0, 25.0, -24.999251562526105, 24.999251562526105, 0.0, 20.0], - rel=1e-6, - abs=1e-8, - ) - else: - assert dataset.bounds == pytest.approx( - [-25.0, 25.0, -25.000000000000277, 25.000000000000096, 0.0, 20.0], - rel=1e-6, - abs=1e-8, - ) - - # Tessellate the component by merging it to a single dataset. - mesh = comp.tessellate(merge_component=True) + # Tessellate the component - always a single dataset + mesh = comp.tessellate() comp.plot() assert "PolyData" in str(mesh) if modeler.client.backend_type != BackendType.LINUX_SERVICE: @@ -159,7 +140,7 @@ def test_component_tessellate(modeler: Modeler): assert mesh.n_faces == 3280 assert mesh.n_arrays == 0 assert mesh.n_points == 3300 - assert dataset.bounds == pytest.approx( + assert mesh.bounds == pytest.approx( [-25.0, 25.0, -24.999251562526105, 24.999251562526105, 0.0, 20.0], rel=1e-6, abs=1e-8, @@ -169,7 +150,7 @@ def test_component_tessellate(modeler: Modeler): assert mesh.n_faces == 15872 assert mesh.n_arrays == 0 assert mesh.n_points == 15892 - assert dataset.bounds == pytest.approx( + assert mesh.bounds == pytest.approx( [-25.0, 25.0, -25.0, 25.0, 0.0, 20.0], rel=1e-6, abs=1e-8,