Skip to content

XY transposable plots V3 part 1: Geometry and Structure (issue #1072) #2687

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- `Geometry.plot()` and `Structure().plot()` now accept the `transpose=True` argument which swaps the horizontal and vertical axes of the plot.

### Changed
- By default, batch downloads will skip files that already exist locally. To force re-downloading and replace existing files, pass the `replace_existing=True` argument to `Batch.load()`, `Batch.download()`, or `BatchData.load()`.
- The `BatchData.load_sim_data()` function now overwrites any previously downloaded simulation files (instead of skipping them).
Expand Down
33 changes: 19 additions & 14 deletions tests/test_components/test_geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,15 @@
_, AX = plt.subplots()


@pytest.mark.parametrize("component", GEO_TYPES)
def test_plot(component):
_ = component.plot(z=0, ax=AX)
@pytest.mark.parametrize("component, transpose", zip(GEO_TYPES, [True, False]))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: The parametrize decorator uses zip(GEO_TYPES, [True, False]) which will only test each geometry type with one transpose value. This should be product(GEO_TYPES, [True, False]) to test all combinations.

Suggested change
@pytest.mark.parametrize("component, transpose", zip(GEO_TYPES, [True, False]))
@pytest.mark.parametrize("component, transpose", list(itertools.product(GEO_TYPES, [True, False])))

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest simply doubling up on pytest.mark.parameterize eg.

@pytest.mark.parameterize("component" , ...)
@pytest.mark.parameterize("transpose" , ...)

def test_plot(component, transpose):
_ = component.plot(z=0, ax=AX, transpose=transpose)
plt.close()


def test_plot_with_units():
_ = BOX.plot(z=0, ax=AX, plot_length_units="nm")
@pytest.mark.parametrize("transpose", [True, False])
def test_plot_with_units(transpose):
_ = BOX.plot(z=0, ax=AX, plot_length_units="nm", transpose=transpose)
plt.close()


Expand Down Expand Up @@ -768,13 +769,12 @@ def test_geometry_touching_intersections_plane(x0):


def test_pop_axis():
b = td.Box(size=(1, 1, 1))
for axis in range(3):
coords = (1, 2, 3)
Lz, (Lx, Ly) = b.pop_axis(coords, axis=axis)
_coords = b.unpop_axis(Lz, (Lx, Ly), axis=axis)
Lz, (Lx, Ly) = td.Box.pop_axis(coords, axis=axis)
_coords = td.Box.unpop_axis(Lz, (Lx, Ly), axis=axis)
assert all(c == _c for (c, _c) in zip(coords, _coords))
_Lz, (_Lx, _Ly) = b.pop_axis(_coords, axis=axis)
_Lz, (_Lx, _Ly) = td.Box.pop_axis(_coords, axis=axis)
assert Lz == _Lz
assert Lx == _Lx
assert Ly == _Ly
Expand Down Expand Up @@ -939,7 +939,8 @@ def test_to_gds(geometry, tmp_path):
assert len(cell.polygons) == 0


def test_custom_surface_geometry(tmp_path):
@pytest.mark.parametrize("transpose", [True, False])
def test_custom_surface_geometry(transpose, tmp_path):
# create tetrahedron STL
vertices = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]])
faces = np.array([[1, 2, 3], [0, 3, 2], [0, 1, 3], [0, 2, 1]])
Expand Down Expand Up @@ -972,9 +973,13 @@ def test_custom_surface_geometry(tmp_path):
assert np.isclose(geom.volume(), 1 / 6)

# test intersections
assert shapely.equals(geom.intersections_plane(x=0), shapely.Polygon([[0, 0], [0, 1], [1, 0]]))
assert shapely.equals(
geom.intersections_plane(z=0.5), shapely.Polygon([[0, 0], [0, 0.5], [0.5, 0]])
geom.intersections_plane(x=0),
shapely.Polygon([[0, 0], [0, 1], [1, 0]]),
)
assert shapely.equals(
geom.intersections_plane(z=0.5),
shapely.Polygon([[0, 0], [0, 0.5], [0.5, 0]]),
)

# test inside
Expand All @@ -983,7 +988,7 @@ def test_custom_surface_geometry(tmp_path):

# test plot
_, ax = plt.subplots()
_ = geom.plot(z=0.1, ax=ax)
_ = geom.plot(z=0.1, ax=ax, transpose=transpose)
plt.close()

# test inconsistent winding
Expand Down Expand Up @@ -1033,7 +1038,7 @@ def test_custom_surface_geometry(tmp_path):
boundary_spec=td.BoundarySpec.all_sides(td.PML()),
)
_, ax = plt.subplots()
_ = sim.plot(y=0, ax=ax)
_ = sim.plot(y=0, ax=ax, transpose=transpose)
plt.close()

# allow small triangles
Expand Down
29 changes: 29 additions & 0 deletions tests/test_components/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Tests objects shared by multiple components."""

from __future__ import annotations

import random

import pytest
from shapely.geometry import Point

from tidy3d.components.utils import pop_axis_and_swap, shape_swap_xy, unpop_axis_and_swap


@pytest.mark.parametrize("transpose", [True, False])
def test_pop_axis_and_swap(transpose):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should also check if the swapping is done properly

for axis in range(3):
coords = (1, 2, 3)
Lz, (Lx, Ly) = pop_axis_and_swap(coords, axis=axis, transpose=transpose)
_coords = unpop_axis_and_swap(Lz, (Lx, Ly), axis=axis, transpose=transpose)
assert all(c == _c for (c, _c) in zip(coords, _coords))
_Lz, (_Lx, _Ly) = pop_axis_and_swap(_coords, axis=axis, transpose=transpose)
assert Lz == _Lz
assert Lx == _Lx
assert Ly == _Ly


def test_shape_swap_xy():
p_orig = Point(random.random(), random.random())
p_new = shape_swap_xy(p_orig)
assert (p_new.coords[0][0], p_new.coords[0][1]) == (p_orig.coords[0][1], p_orig.coords[0][0])
18 changes: 12 additions & 6 deletions tidy3d/components/data/unstructured/triangular.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
SpatialDataArray,
)
from tidy3d.components.types import ArrayLike, Ax, Axis, Bound
from tidy3d.components.utils import pop_axis_and_swap
Copy link
Contributor Author

@jewettaijfc jewettaijfc Jul 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: The modifications to the triangular.py were probably not necessary for Geometry or Structure plotting. But I'd like to include these changes in this PR anyway. The ability to plot triangle meshes is needed by several other PRs which are coming soon, so I want to land them in this PR first.

Copy link

@jewettaij jewettaij Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those subsequent changes are currently bundled into a single PR (PR2544), which is based on top of this PR.

Regarding this PR:

I verified this function is working by running the plot_mesh() function in the ChargeSolver.ipynb example with transpose=True, and verifying that the horizontal and vertical axes were swapped. However you will need the other changes in PR2544 for that particular example to work, because it does not invoke this function directly. Perhaps the changes to this file should be moved into that PR, since the other code modified in this PR does not depend on this file.

from tidy3d.components.viz import add_ax_if_none, equal_aspect, plot_params_grid
from tidy3d.constants import inf
from tidy3d.exceptions import DataError
Expand Down Expand Up @@ -570,9 +571,10 @@ def does_cover(self, bounds: Bound) -> bool:

""" Plotting """

@property
def _triangulation_obj(self) -> Triangulation:
def _triangulation_obj(self, transpose: bool = False) -> Triangulation:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Converting this from a @property to a method breaks backward compatibility for any code that accessed _triangulation_obj as a property

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree, should be added back

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry. That was bad. I should have defined a new function here.

"""Matplotlib triangular representation of the grid to use in plotting."""
if transpose:
return Triangulation(self.points[:, 1], self.points[:, 0], self.cells)
return Triangulation(self.points[:, 0], self.points[:, 1], self.cells)

@equal_aspect
Expand All @@ -589,6 +591,7 @@ def plot(
shading: Literal["gourand", "flat"] = "gouraud",
cbar_kwargs: Optional[dict] = None,
pcolor_kwargs: Optional[dict] = None,
transpose: bool = False,
) -> Ax:
"""Plot the data field and/or the unstructured grid.

Expand Down Expand Up @@ -616,6 +619,8 @@ def plot(
Additional parameters passed to colorbar object.
pcolor_kwargs: Dict = {}
Additional parameters passed to ax.tripcolor()
transpose : bool = False
Swap horizontal and vertical axes. (This overrides the default ascending axis order)

Returns
-------
Expand All @@ -639,7 +644,7 @@ def plot(
f"{self._values_coords_dict} before plotting."
)
plot_obj = ax.tripcolor(
self._triangulation_obj,
self._triangulation_obj(transpose=transpose),
self.values.data.ravel(),
shading=shading,
cmap=cmap,
Expand All @@ -657,14 +662,15 @@ def plot(
# plot grid if requested
if grid:
ax.triplot(
self._triangulation_obj,
self._triangulation_obj(transpose=transpose),
color=plot_params_grid.edgecolor,
linewidth=plot_params_grid.linewidth,
)

# set labels and titles
ax_labels = ["x", "y", "z"]
normal_axis_name = ax_labels.pop(self.normal_axis)
normal_axis_name, ax_labels = pop_axis_and_swap(
"xyz", self.normal_axis, transpose=transpose
)
ax.set_xlabel(ax_labels[0])
ax.set_ylabel(ax_labels[1])
ax.set_title(f"{normal_axis_name} = {self.normal_pos}")
Expand Down
Loading