diff --git a/tests/test_components/test_eme.py b/tests/test_components/test_eme.py index f058ee710..85a2d95f3 100644 --- a/tests/test_components/test_eme.py +++ b/tests/test_components/test_eme.py @@ -283,21 +283,22 @@ def test_eme_monitor(): ) -def test_eme_simulation(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_eme_simulation(transpose): sim = make_eme_sim() - _ = sim.plot(x=0, ax=AX) - _ = sim.plot(y=0, ax=AX) - _ = sim.plot(z=0, ax=AX) - _ = sim.plot_grid(x=0, ax=AX) - _ = sim.plot_grid(y=0, ax=AX) - _ = sim.plot_grid(z=0, ax=AX) - _ = sim.plot_eps(x=0, ax=AX) - _ = sim.plot_eps(y=0, ax=AX) - _ = sim.plot_eps(z=0, ax=AX) + _ = sim.plot(x=0, ax=AX, transpose=transpose) + _ = sim.plot(y=0, ax=AX, transpose=transpose) + _ = sim.plot(z=0, ax=AX, transpose=transpose) + _ = sim.plot_grid(x=0, ax=AX, transpose=transpose) + _ = sim.plot_grid(y=0, ax=AX, transpose=transpose) + _ = sim.plot_grid(z=0, ax=AX, transpose=transpose) + _ = sim.plot_eps(x=0, ax=AX, transpose=transpose) + _ = sim.plot_eps(y=0, ax=AX, transpose=transpose) + _ = sim.plot_eps(z=0, ax=AX, transpose=transpose) sim2 = sim.updated_copy(axis=1) - _ = sim2.plot(x=0, ax=AX) - _ = sim2.plot(y=0, ax=AX) - _ = sim2.plot(z=0, ax=AX) + _ = sim2.plot(x=0, ax=AX, transpose=transpose) + _ = sim2.plot(y=0, ax=AX, transpose=transpose) + _ = sim2.plot(z=0, ax=AX, transpose=transpose) # need at least one freq with pytest.raises(pd.ValidationError): diff --git a/tests/test_components/test_geometry.py b/tests/test_components/test_geometry.py index 5237abdac..d0eb056b6 100644 --- a/tests/test_components/test_geometry.py +++ b/tests/test_components/test_geometry.py @@ -85,14 +85,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])) +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() @@ -204,11 +205,11 @@ def test_array_to_vertices(): assert np.all(np.array(vertices) == np.array(vertices2)) -@pytest.mark.parametrize("component", GEO_TYPES) -def test_intersections_plane(component): - assert len(component.intersections_plane(z=0.2)) > 0 - assert len(component.intersections_plane(x=0.2)) > 0 - assert len(component.intersections_plane(x=10000)) == 0 +@pytest.mark.parametrize("component, transpose", zip(GEO_TYPES, [True, False])) +def test_intersections_plane(component, transpose): + assert len(component.intersections_plane(z=0.2, transpose=transpose)) > 0 + assert len(component.intersections_plane(x=0.2, transpose=transpose)) > 0 + assert len(component.intersections_plane(x=10000, transpose=transpose)) == 0 def test_intersections_plane_inf(): @@ -766,36 +767,49 @@ 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) = td.Box.pop_axis(_coords, axis=axis) + assert Lz == _Lz + assert Lx == _Lx + assert Ly == _Ly + + +@pytest.mark.parametrize("transpose", [True, False]) +def test_pop_axis_and_swap(transpose): + for axis in range(3): + coords = (1, 2, 3) + Lz, (Lx, Ly) = td.Box.pop_axis_and_swap(coords, axis=axis, transpose=transpose) + _coords = td.Box.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) = b.pop_axis(_coords, axis=axis) + _Lz, (_Lx, _Ly) = td.Box.pop_axis_and_swap(_coords, axis=axis, transpose=transpose) assert Lz == _Lz assert Lx == _Lx assert Ly == _Ly -def test_2b_box_intersections(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_2b_box_intersections(transpose): plane = td.Box(size=(1, 4, 0)) box1 = td.Box(size=(1, 1, 1)) box2 = td.Box(size=(1, 1, 1), center=(3, 0, 0)) - result = plane.intersections_with(box1) + result = plane.intersections_with(box1, transpose=transpose) assert len(result) == 1 assert result[0].geom_type == "Polygon" - assert len(plane.intersections_with(box2)) == 0 + assert len(plane.intersections_with(box2, transpose=transpose)) == 0 with pytest.raises(ValidationError): - _ = box1.intersections_with(box2) + _ = box1.intersections_with(box2, transpose=transpose) - assert len(box1.intersections_2dbox(plane)) == 1 - assert len(box2.intersections_2dbox(plane)) == 0 + assert len(box1.intersections_2dbox(plane, transpose=transpose)) == 1 + assert len(box2.intersections_2dbox(plane, transpose=transpose)) == 0 with pytest.raises(ValidationError): - _ = box2.intersections_2dbox(box1) + _ = box2.intersections_2dbox(box1, transpose=transpose) def test_polyslab_merge(): @@ -937,7 +951,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]]) @@ -970,9 +985,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, transpose=transpose), + shapely.Polygon([[0, 0], [0, 1], [1, 0]]), + ) + assert shapely.equals( + geom.intersections_plane(z=0.5, transpose=transpose), + shapely.Polygon([[0, 0], [0, 0.5], [0.5, 0]]), ) # test inside @@ -981,7 +1000,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 @@ -1031,7 +1050,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 diff --git a/tests/test_components/test_heat.py b/tests/test_components/test_heat.py index ccf29af9d..42b7b2c81 100644 --- a/tests/test_components/test_heat.py +++ b/tests/test_components/test_heat.py @@ -335,11 +335,12 @@ def make_heat_sim(include_custom_source: bool = True): return heat_sim -def test_heat_sim(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_heat_sim(transpose): bc_temp, bc_flux, bc_conv = make_heat_bcs() heat_sim = make_heat_sim() - _ = heat_sim.plot(x=0) + _ = heat_sim.plot(x=0, transpose=transpose) # wrong names given for pl in [ @@ -373,14 +374,14 @@ def test_heat_sim(): with pytest.raises(pd.ValidationError): heat_sim.updated_copy(monitors=[temp_mnt, temp_mnt]) - _ = heat_sim.plot(x=0) + _ = heat_sim.plot(x=0, transpose=transpose) plt.close() - _ = heat_sim.plot_heat_conductivity(y=0) + _ = heat_sim.plot_heat_conductivity(y=0, transpose=transpose) plt.close() heat_sim_sym = heat_sim.updated_copy(symmetry=(0, 1, 1)) - _ = heat_sim_sym.plot_heat_conductivity(z=0, colorbar="source") + _ = heat_sim_sym.plot_heat_conductivity(z=0, colorbar="source", transpose=transpose) plt.close() # no negative symmetry diff --git a/tests/test_components/test_heat_charge.py b/tests/test_components/test_heat_charge.py index 09a61b12d..053bbb461 100644 --- a/tests/test_components/test_heat_charge.py +++ b/tests/test_components/test_heat_charge.py @@ -852,7 +852,8 @@ def test_heat_charge_sources(structures): _ = td.HeatSource(structures=["solid_structure"], rate="100") -def test_heat_charge_simulation(simulation_data): +@pytest.mark.parametrize("transpose", [True, False]) +def test_heat_charge_simulation(transpose, simulation_data): """Tests 'HeatChargeSimulation' and 'ConductionSimulation' objects.""" heat_sim_data, cond_sim_data, voltage_capacitance_sim_data, current_voltage_simulation_data = ( simulation_data @@ -876,6 +877,18 @@ def test_heat_charge_simulation(simulation_data): "Current-Voltage simulation should be created successfully." ) + _ = heat_sim.plot_heat_conductivity(x=0, transpose=transpose) + plt.close() + _ = heat_sim.plot_property(x=0, property="heat_conductivity", transpose=transpose) + plt.close() + _ = cond_sim.plot_property(x=0, property="electric_conductivity", transpose=transpose) + plt.close() + for sim in (heat_sim, cond_sim, voltage_capacitance_sim, current_voltage_sim): + _ = sim.plot_boundaries(x=0, transpose=transpose) + plt.close() + _ = sim.plot_sources(x=0, transpose=transpose) + plt.close() + def test_sim_data_plotting(simulation_data): """Tests whether simulation data can be plotted and appropriate errors are raised.""" @@ -1433,7 +1446,8 @@ def test_plotting_functions(simulation_data): heat_sim_data.plot_field("test", invalid_param=0) -def test_bandgap_monitor(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_bandgap_monitor(transpose): """Test energy bandgap monitor ploting function.""" # create a triangle grid tri_grid_points = td.PointDataArray( @@ -1573,8 +1587,8 @@ def test_bandgap_monitor(): # test check for the voltage value in the list of arguments - tri_single_voltage_data.plot(x=0.0) - tri_multi_voltage_data.plot(x=0.0, voltage=1.0) + tri_single_voltage_data.plot(x=0.0, transpose=transpose) + tri_multi_voltage_data.plot(x=0.0, voltage=1.0, transpose=transpose) with pytest.raises(DataError): tri_multi_voltage_data.plot(x=0.0) diff --git a/tests/test_components/test_scene.py b/tests/test_components/test_scene.py index 0704b9cdc..d0dc16bd6 100644 --- a/tests/test_components/test_scene.py +++ b/tests/test_components/test_scene.py @@ -50,13 +50,15 @@ def test_validate_components_none(): assert SCENE._validate_num_mediums(val=None) is None -def test_plot_eps(): - ax = SCENE_FULL.plot_eps(x=0) +@pytest.mark.parametrize("transpose", [True, False]) +def test_plot_eps(transpose): + ax = SCENE_FULL.plot_eps(x=0, transpose=transpose) SCENE_FULL._add_cbar_eps(eps_min=1, eps_max=2, ax=ax) plt.close() -def test_plot_eps_multiphysics(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_plot_eps_multiphysics(transpose): s = td.Scene( structures=[ td.Structure( @@ -70,20 +72,22 @@ def test_plot_eps_multiphysics(): ] ) assert s.structures[0].medium.name == "SiO2" - s.plot_eps(x=0) + s.plot_eps(x=0, transpose=transpose) -def test_plot_eps_bounds(): - _ = SCENE_FULL.plot_eps(x=0, hlim=[-0.45, 0.45]) +@pytest.mark.parametrize("transpose", [True, False]) +def test_plot_eps_bounds(transpose): + _ = SCENE_FULL.plot_eps(x=0, hlim=[-0.45, 0.45], transpose=transpose) plt.close() - _ = SCENE_FULL.plot_eps(x=0, vlim=[-0.45, 0.45]) + _ = SCENE_FULL.plot_eps(x=0, vlim=[-0.45, 0.45], transpose=transpose) plt.close() - _ = SCENE_FULL.plot_eps(x=0, hlim=[-0.45, 0.45], vlim=[-0.45, 0.45]) + _ = SCENE_FULL.plot_eps(x=0, hlim=[-0.45, 0.45], vlim=[-0.45, 0.45], transpose=transpose) plt.close() -def test_plot(): - SCENE_FULL.plot(x=0) +@pytest.mark.parametrize("transpose", [True, False]) +def test_plot(transpose): + SCENE_FULL.plot(x=0, transpose=transpose) plt.close() @@ -93,44 +97,48 @@ def test_plot_1d_scene(): plt.close() -def test_plot_bounds(): - _ = SCENE_FULL.plot(x=0, hlim=[-0.45, 0.45]) +@pytest.mark.parametrize("transpose", [True, False]) +def test_plot_bounds(transpose): + _ = SCENE_FULL.plot(x=0, hlim=[-0.45, 0.45], transpose=transpose) plt.close() - _ = SCENE_FULL.plot(x=0, vlim=[-0.45, 0.45]) + _ = SCENE_FULL.plot(x=0, vlim=[-0.45, 0.45], transpose=transpose) plt.close() - _ = SCENE_FULL.plot(x=0, hlim=[-0.45, 0.45], vlim=[-0.45, 0.45]) + _ = SCENE_FULL.plot(x=0, hlim=[-0.45, 0.45], vlim=[-0.45, 0.45], transpose=transpose) plt.close() -def test_structure_alpha(): - _ = SCENE_FULL.plot_structures_eps(x=0, alpha=None) +@pytest.mark.parametrize("transpose", [True, False]) +def test_structure_alpha(transpose): + _ = SCENE_FULL.plot_structures_eps(x=0, alpha=None, transpose=transpose) plt.close() - _ = SCENE_FULL.plot_structures_eps(x=0, alpha=-1) + _ = SCENE_FULL.plot_structures_eps(x=0, alpha=-1, transpose=transpose) plt.close() - _ = SCENE_FULL.plot_structures_eps(x=0, alpha=1) + _ = SCENE_FULL.plot_structures_eps(x=0, alpha=1, transpose=transpose) plt.close() - _ = SCENE_FULL.plot_structures_eps(x=0, alpha=0.5) + _ = SCENE_FULL.plot_structures_eps(x=0, alpha=0.5, transpose=transpose) plt.close() - _ = SCENE_FULL.plot_structures_eps(x=0, alpha=0.5, cbar=True) + _ = SCENE_FULL.plot_structures_eps(x=0, alpha=0.5, cbar=True, transpose=transpose) plt.close() new_structs = [ td.Structure(geometry=s.geometry, medium=SCENE_FULL.medium) for s in SCENE_FULL.structures ] S2 = SCENE_FULL.copy(update={"structures": new_structs}) - _ = S2.plot_structures_eps(x=0, alpha=0.5) + _ = S2.plot_structures_eps(x=0, alpha=0.5, transpose=transpose) plt.close() -def test_plot_with_units(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_plot_with_units(transpose): scene_with_units = SCENE_FULL.updated_copy(plot_length_units="nm") - scene_with_units.plot(x=-0.5) + scene_with_units.plot(x=-0.5, transpose=transpose) -def test_filter_structures(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_filter_structures(transpose): s1 = td.Structure(geometry=td.Box(size=(1, 1, 1)), medium=SCENE.medium) s2 = td.Structure(geometry=td.Box(size=(1, 1, 1), center=(1, 1, 1)), medium=SCENE.medium) plane = td.Box(center=(0, 0, 1.5), size=(td.inf, td.inf, 0)) - SCENE._filter_structures_plane_medium(structures=[s1, s2], plane=plane) + SCENE._filter_structures_plane_medium(structures=[s1, s2], plane=plane, transpose=transpose) def test_get_structure_plot_params(): diff --git a/tests/test_components/test_simulation.py b/tests/test_components/test_simulation.py index 999ca4a00..c748739eb 100644 --- a/tests/test_components/test_simulation.py +++ b/tests/test_components/test_simulation.py @@ -711,22 +711,25 @@ def test_wvl_mat_min_error(): SIM.wvl_mat_min() -def test_plot_structure(): - _ = SIM_FULL.structures[0].plot(x=0) +@pytest.mark.parametrize("transpose", [True, False]) +def test_plot_structure(transpose): + _ = SIM_FULL.structures[0].plot(x=0, transpose=transpose) plt.close() -def test_plot_eps(): - _ = SIM_FULL.plot_eps(x=0) +@pytest.mark.parametrize("transpose", [True, False]) +def test_plot_eps(transpose): + _ = SIM_FULL.plot_eps(x=0, transpose=transpose) plt.close() -def test_plot_eps_bounds(): - _ = SIM_FULL.plot_eps(x=0, hlim=[-0.45, 0.45]) +@pytest.mark.parametrize("transpose", [True, False]) +def test_plot_eps_bounds(transpose): + _ = SIM_FULL.plot_eps(x=0, hlim=[-0.45, 0.45], transpose=transpose) plt.close() - _ = SIM_FULL.plot_eps(x=0, vlim=[-0.45, 0.45]) + _ = SIM_FULL.plot_eps(x=0, vlim=[-0.45, 0.45], transpose=transpose) plt.close() - _ = SIM_FULL.plot_eps(x=0, hlim=[-0.45, 0.45], vlim=[-0.45, 0.45]) + _ = SIM_FULL.plot_eps(x=0, hlim=[-0.45, 0.45], vlim=[-0.45, 0.45], transpose=transpose) plt.close() @@ -824,14 +827,14 @@ def test_bad_eps_arg(self, eps_comp): self.make_sim(self.medium_diag).plot_eps(x=0, eps_component=eps_comp) @pytest.mark.parametrize( - "eps_comp", - [None, *diag_comps], + "eps_comp, transpose", + zip([None, *diag_comps], [True, False]), ) - def test_plot_anisotropic_medium(self, eps_comp): + def test_plot_anisotropic_medium(self, eps_comp, transpose): """Test plotting diagonal components of a diagonally anisotropic medium succeeds or not. diagonal components and ``None`` should succeed. """ - self.make_sim(self.medium_diag).plot_eps(x=0, eps_component=eps_comp) + self.make_sim(self.medium_diag).plot_eps(x=0, eps_component=eps_comp, transpose=transpose) @pytest.mark.parametrize("eps_comp", offdiag_comps) def test_plot_anisotropic_medium_offdiagfail(self, eps_comp): @@ -855,15 +858,15 @@ def test_plot_anisotropic_medium_diff(self, tmp_path, eps_comp1, eps_comp2, expe self.compare_eps_images(tmp_path, eps_comp1, eps_comp2, expected, self.medium_diag) @pytest.mark.parametrize( - "eps_comp", - [None, *diag_comps, *offdiag_comps], + "eps_comp, transpose", + zip([None, *diag_comps, *offdiag_comps], [True, False]), ) - def test_plot_fully_anisotropic_medium(self, eps_comp): + def test_plot_fully_anisotropic_medium(self, eps_comp, transpose): """Test plotting all components of a fully anisotropic medium. All plots should succeed. """ sim = self.make_sim(self.medium_fullyani) - sim.plot_eps(x=0, eps_component=eps_comp) + sim.plot_eps(x=0, eps_component=eps_comp, transpose=transpose) # Test parameters for comparing plots of a FullyAnisotropicMedium fullyani_testplot_diff_params = [] @@ -882,14 +885,14 @@ def test_plot_fully_anisotropic_medium_diff(self, tmp_path, eps_comp1, eps_comp2 self.compare_eps_images(tmp_path, eps_comp1, eps_comp2, expected, self.medium_fullyani) @pytest.mark.parametrize( - "eps_comp", - [None, *diag_comps], + "eps_comp, transpose", + zip([None, *diag_comps], [True, False]), ) - def test_plot_customanisotropic_medium(self, eps_comp, medium_customani): + def test_plot_customanisotropic_medium(self, eps_comp, transpose, medium_customani): """Test plotting diagonal components of a diagonally anisotropic custom medium. diagonal components and ``None`` should succeed. """ - self.make_sim(medium_customani).plot_eps(x=0, eps_component=eps_comp) + self.make_sim(medium_customani).plot_eps(x=0, eps_component=eps_comp, transpose=transpose) @pytest.mark.parametrize("eps_comp", offdiag_comps) def test_plot_customanisotropic_medium_offdiagfail(self, eps_comp, medium_customani): @@ -915,14 +918,16 @@ def test_plot_customanisotropic_medium_diff( self.compare_eps_images(tmp_path, eps_comp1, eps_comp2, expected, medium_customani) -def test_plot(): - SIM_FULL.plot(x=0) +@pytest.mark.parametrize("transpose", [True, False]) +def test_plot(transpose): + SIM_FULL.plot(x=0, transpose=transpose) plt.close() -def test_plot_with_units(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_plot_with_units(transpose): sim_with_units = SIM_FULL.updated_copy(plot_length_units="nm") - sim_with_units.plot(x=-0.5) + sim_with_units.plot(x=-0.5, transpose=transpose) def test_plot_1d_sim(): @@ -938,12 +943,13 @@ def test_plot_1d_sim(): plt.close() -def test_plot_bounds(): - _ = SIM_FULL.plot(x=0, hlim=[-0.45, 0.45]) +@pytest.mark.parametrize("transpose", [True, False]) +def test_plot_bounds(transpose): + _ = SIM_FULL.plot(x=0, hlim=[-0.45, 0.45], transpose=transpose) plt.close() - _ = SIM_FULL.plot(x=0, vlim=[-0.45, 0.45]) + _ = SIM_FULL.plot(x=0, vlim=[-0.45, 0.45], transpose=transpose) plt.close() - _ = SIM_FULL.plot(x=0, hlim=[-0.45, 0.45], vlim=[-0.45, 0.45]) + _ = SIM_FULL.plot(x=0, hlim=[-0.45, 0.45], vlim=[-0.45, 0.45], transpose=transpose) plt.close() @@ -952,22 +958,23 @@ def test_plot_3d(): plt.close() -def test_structure_alpha(): - _ = SIM_FULL.plot_structures_eps(x=0, alpha=None) +@pytest.mark.parametrize("transpose", [True, False]) +def test_structure_alpha(transpose): + _ = SIM_FULL.plot_structures_eps(x=0, alpha=None, transpose=transpose) plt.close() - _ = SIM_FULL.plot_structures_eps(x=0, alpha=-1) + _ = SIM_FULL.plot_structures_eps(x=0, alpha=-1, transpose=transpose) plt.close() - _ = SIM_FULL.plot_structures_eps(x=0, alpha=1) + _ = SIM_FULL.plot_structures_eps(x=0, alpha=1, transpose=transpose) plt.close() - _ = SIM_FULL.plot_structures_eps(x=0, alpha=0.5) + _ = SIM_FULL.plot_structures_eps(x=0, alpha=0.5, transpose=transpose) plt.close() - _ = SIM_FULL.plot_structures_eps(x=0, alpha=0.5, cbar=True) + _ = SIM_FULL.plot_structures_eps(x=0, alpha=0.5, cbar=True, transpose=transpose) plt.close() new_structs = [ td.Structure(geometry=s.geometry, medium=SIM_FULL.medium) for s in SIM_FULL.structures ] S2 = SIM_FULL.copy(update={"structures": new_structs}) - _ = S2.plot_structures_eps(x=0, alpha=0.5) + _ = S2.plot_structures_eps(x=0, alpha=0.5, transpose=transpose) plt.close() @@ -1007,22 +1014,25 @@ def test_plot_eps_with_default_frequency(): plt.close() -def test_plot_symmetries(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_plot_symmetries(transpose): S2 = SIM.copy(update={"symmetry": (1, 0, -1)}) - S2.plot_symmetries(x=0) + S2.plot_symmetries(x=0, transpose=transpose) plt.close() -def test_plot_grid(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_plot_grid(transpose): override = td.Structure(geometry=td.Box(size=(1, 1, 1)), medium=td.Medium()) S2 = SIM_FULL.copy( update={"grid_spec": td.GridSpec(wavelength=1.0, override_structures=[override])} ) - S2.plot_grid(x=0) + S2.plot_grid(x=0, transpose=transpose) plt.close() -def test_plot_boundaries(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_plot_boundaries(transpose): bound_spec = td.BoundarySpec( x=td.Boundary(plus=td.PECBoundary(), minus=td.PMCBoundary()), y=td.Boundary( @@ -1032,16 +1042,17 @@ def test_plot_boundaries(): z=td.Boundary(plus=td.Periodic(), minus=td.Periodic()), ) S2 = SIM_FULL.copy(update={"boundary_spec": bound_spec}) - S2.plot_boundaries(z=0) + S2.plot_boundaries(z=0, transpose=transpose) plt.close() -def test_plot_with_lumped_elements(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_plot_with_lumped_elements(transpose): load = td.LumpedResistor( center=(0, 0, 0), size=(1, 2, 0), name="resistor", voltage_axis=0, resistance=50 ) sim_test = SIM_FULL.updated_copy(lumped_elements=[load]) - sim_test.plot(z=0) + sim_test.plot(z=0, transpose=transpose) plt.close() @@ -2890,9 +2901,11 @@ def test_sim_subsection(unstructured, nz): boundary_spec=td.BoundarySpec.all_sides(td.Periodic()), # Set theta to 'pi/2' for 2D simulation in the x-y plane monitors=[ - mnt.updated_copy(theta=np.pi / 2) - if isinstance(mnt, td.FieldProjectionAngleMonitor) - else mnt + ( + mnt.updated_copy(theta=np.pi / 2) + if isinstance(mnt, td.FieldProjectionAngleMonitor) + else mnt + ) for mnt in subsection_monitors if not isinstance( mnt, (td.FieldProjectionCartesianMonitor, td.FieldProjectionKSpaceMonitor) diff --git a/tests/test_components/test_source.py b/tests/test_components/test_source.py index e4e4cc6db..fc7c77539 100644 --- a/tests/test_components/test_source.py +++ b/tests/test_components/test_source.py @@ -148,19 +148,20 @@ def test_dipole(): _ = td.PointDipole(size=(1, 1, 1), source_time=g, center=(1, 2, 3), polarization="Ex") -def test_FieldSource(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_FieldSource(transpose): g = td.GaussianPulse(freq0=1e12, fwidth=0.1e12) mode_spec = td.ModeSpec(num_modes=2) # test we can make planewave - _ = td.PlaneWave(size=(0, td.inf, td.inf), source_time=g, pol_angle=np.pi / 2, direction="+") - # s.plot(y=0) - # plt.close() + s = td.PlaneWave(size=(0, td.inf, td.inf), source_time=g, pol_angle=np.pi / 2, direction="+") + s.plot(y=0, transpose=transpose) + plt.close() # test we can make gaussian beam - _ = td.GaussianBeam(size=(0, 1, 1), source_time=g, pol_angle=np.pi / 2, direction="+") - # s.plot(y=0) - # plt.close() + s = td.GaussianBeam(size=(0, 1, 1), source_time=g, pol_angle=np.pi / 2, direction="+") + s.plot(y=0, transpose=transpose) + plt.close() # test we can make an astigmatic gaussian beam _ = td.AstigmaticGaussianBeam( @@ -173,11 +174,13 @@ def test_FieldSource(): ) # test we can make mode source - _ = td.ModeSource( + s = td.ModeSource( size=(0, 1, 1), direction="+", source_time=g, mode_spec=mode_spec, mode_index=0 ) - # s.plot(y=0) - # plt.close() + s.plot(y=0, transpose=transpose) + plt.close() + s.plot(z=0, transpose=transpose) + plt.close() # test that non-planar geometry crashes plane wave and gaussian beams with pytest.raises(pydantic.ValidationError): @@ -203,8 +206,20 @@ def test_FieldSource(): with pytest.raises(pydantic.ValidationError): _ = td.TFSF(size=(1, 1, 0), direction="+", source_time=g, injection_axis=2) - # s.plot(z=0) - # plt.close() + +@pytest.mark.parametrize("transpose", [True, False]) +def test_current_source(transpose): + L: float = 5.0 + source = td.UniformCurrentSource( + center=(0, -L / 3, 0), + size=(L, 0, L / 2), + polarization="Ex", + source_time=td.GaussianPulse( + freq0=100e14, + fwidth=10e14, + ), + ) + source.plot(z=0, transpose=transpose) def test_pol_arrow(): diff --git a/tests/test_plugins/test_invdes.py b/tests/test_plugins/test_invdes.py index 0cb2a399f..b91390108 100644 --- a/tests/test_plugins/test_invdes.py +++ b/tests/test_plugins/test_invdes.py @@ -540,7 +540,10 @@ def test_parameter_spec(spec_class, spec_kwargs, expected_shape): assert params.shape == expected_shape -def test_parameter_spec_with_inverse_design(use_emulated_run, use_emulated_to_sim_data): # noqa: F811 +def test_parameter_spec_with_inverse_design( + use_emulated_run, # noqa: F811 + use_emulated_to_sim_data, +): """Test InitializationSpec with InverseDesign class.""" metric = 2 * ModePower(monitor_name=MNT_NAME2, f=[FREQ0]) ** 2 diff --git a/tests/test_plugins/test_mode_solver.py b/tests/test_plugins/test_mode_solver.py index 9e52ca06e..70acd207a 100644 --- a/tests/test_plugins/test_mode_solver.py +++ b/tests/test_plugins/test_mode_solver.py @@ -1068,7 +1068,8 @@ def test_mode_solver_relative(): _ = ms._data_on_yee_grid_relative(basis=basis) -def test_mode_solver_plot(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_mode_solver_plot(transpose): """Test mode plane plotting functions""" simulation = td.Simulation( @@ -1095,13 +1096,15 @@ def test_mode_solver_plot(): colocate=False, ) _, ax = plt.subplots(2, 2, figsize=(12, 8), tight_layout=True) - ms.plot(ax=ax[0, 0]) - ms.plot_eps(freq=200e14, alpha=0.7, ax=ax[0, 1]) - ms.plot_structures_eps(freq=200e14, alpha=0.8, cbar=True, reverse=False, ax=ax[1, 0]) - ms.plot_grid(linewidth=0.3, ax=ax[1, 0]) - ms.plot(ax=ax[1, 1]) - ms.plot_pml(ax=ax[1, 1]) - ms.plot_grid(linewidth=0.3, ax=ax[1, 1]) + ms.plot(ax=ax[0, 0], transpose=transpose) + ms.plot_eps(freq=200e14, alpha=0.7, ax=ax[0, 1], transpose=transpose) + ms.plot_structures_eps( + freq=200e14, alpha=0.8, cbar=True, reverse=False, ax=ax[1, 0], transpose=transpose + ) + ms.plot_grid(linewidth=0.3, ax=ax[1, 0], transpose=transpose) + ms.plot(ax=ax[1, 1], transpose=transpose) + ms.plot_pml(ax=ax[1, 1], transpose=transpose) + ms.plot_grid(linewidth=0.3, ax=ax[1, 1], transpose=transpose) plt.close() diff --git a/tidy3d/components/base_sim/simulation.py b/tidy3d/components/base_sim/simulation.py index f6f0e8c90..d273080a8 100644 --- a/tidy3d/components/base_sim/simulation.py +++ b/tidy3d/components/base_sim/simulation.py @@ -251,6 +251,7 @@ def plot( hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, fill_structures: bool = True, + transpose: bool = False, **patch_kwargs, ) -> Ax: """Plot each of simulation's components on a plane defined by one nonzero x,y,z coordinate. @@ -275,6 +276,8 @@ def plot( The z range if plotting on xz or yz planes, y plane if plotting on xy plane. fill_structures : bool = True Whether to fill structures with color or just draw outlines. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- matplotlib.axes._subplots.Axes @@ -282,23 +285,34 @@ def plot( """ hlim, vlim = Scene._get_plot_lims( - bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim + bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose ) ax = self.scene.plot_structures( - ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, fill=fill_structures + ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, fill=fill_structures, transpose=transpose + ) + ax = self.plot_sources( + ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=source_alpha, transpose=transpose + ) + ax = self.plot_monitors( + ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=monitor_alpha, transpose=transpose ) - ax = self.plot_sources(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=source_alpha) - ax = self.plot_monitors(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=monitor_alpha) ax = Scene._set_plot_bounds( - bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + bounds=self.simulation_bounds, + ax=ax, + x=x, + y=y, + z=z, + hlim=hlim, + vlim=vlim, + transpose=transpose, ) - ax = self.plot_boundaries(ax=ax, x=x, y=y, z=z) - ax = self.plot_symmetries(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + ax = self.plot_boundaries(ax=ax, x=x, y=y, z=z, transpose=transpose) + ax = self.plot_symmetries(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose) # Add the default axis labels, tick labels, and title ax = Box.add_ax_labels_and_title( - ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units + ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units, transpose=transpose ) return ax @@ -314,6 +328,7 @@ def plot_sources( vlim: Optional[tuple[float, float]] = None, alpha: Optional[float] = None, ax: Ax = None, + transpose: bool = False, ) -> Ax: """Plot each of simulation's sources on a plane defined by one nonzero x,y,z coordinate. @@ -333,6 +348,8 @@ def plot_sources( Opacity of the sources, If ``None`` uses Tidy3d default. ax : matplotlib.axes._subplots.Axes = None Matplotlib axes to plot on, if not specified, one is created. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -341,13 +358,22 @@ def plot_sources( """ bounds = self.bounds for source in self.sources: - ax = source.plot(x=x, y=y, z=z, alpha=alpha, ax=ax, sim_bounds=bounds) + ax = source.plot( + x=x, y=y, z=z, alpha=alpha, ax=ax, sim_bounds=bounds, transpose=transpose + ) ax = Scene._set_plot_bounds( - bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + bounds=self.simulation_bounds, + ax=ax, + x=x, + y=y, + z=z, + hlim=hlim, + vlim=vlim, + transpose=transpose, ) # Add the default axis labels, tick labels, and title ax = Box.add_ax_labels_and_title( - ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units + ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units, transpose=transpose ) return ax @@ -362,6 +388,7 @@ def plot_monitors( vlim: Optional[tuple[float, float]] = None, alpha: Optional[float] = None, ax: Ax = None, + transpose: bool = False, ) -> Ax: """Plot each of simulation's monitors on a plane defined by one nonzero x,y,z coordinate. @@ -381,6 +408,8 @@ def plot_monitors( Opacity of the sources, If ``None`` uses Tidy3d default. ax : matplotlib.axes._subplots.Axes = None Matplotlib axes to plot on, if not specified, one is created. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -389,13 +418,22 @@ def plot_monitors( """ bounds = self.bounds for monitor in self.monitors: - ax = monitor.plot(x=x, y=y, z=z, alpha=alpha, ax=ax, sim_bounds=bounds) + ax = monitor.plot( + x=x, y=y, z=z, alpha=alpha, ax=ax, sim_bounds=bounds, transpose=transpose + ) ax = Scene._set_plot_bounds( - bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + bounds=self.simulation_bounds, + ax=ax, + x=x, + y=y, + z=z, + hlim=hlim, + vlim=vlim, + transpose=transpose, ) # Add the default axis labels, tick labels, and title ax = Box.add_ax_labels_and_title( - ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units + ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units, transpose=transpose ) return ax @@ -409,6 +447,7 @@ def plot_symmetries( hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, ax: Ax = None, + transpose: bool = False, ) -> Ax: """Plot each of simulation's symmetries on a plane defined by one nonzero x,y,z coordinate. @@ -440,13 +479,20 @@ def plot_symmetries( continue sym_box = self._make_symmetry_box(sym_axis=sym_axis) plot_params = self._make_symmetry_plot_params(sym_value=sym_value) - ax = sym_box.plot(x=x, y=y, z=z, ax=ax, **plot_params.to_kwargs()) + ax = sym_box.plot(x=x, y=y, z=z, ax=ax, transpose=transpose, **plot_params.to_kwargs()) ax = Scene._set_plot_bounds( - bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + bounds=self.simulation_bounds, + ax=ax, + x=x, + y=y, + z=z, + hlim=hlim, + vlim=vlim, + transpose=transpose, ) # Add the default axis labels, tick labels, and title ax = Box.add_ax_labels_and_title( - ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units + ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units, transpose=transpose ) return ax @@ -482,6 +528,7 @@ def plot_boundaries( y: Optional[float] = None, z: Optional[float] = None, ax: Ax = None, + transpose: bool = False, **kwargs, ) -> Ax: """Plot the simulation boundary conditions as lines on a plane @@ -497,6 +544,8 @@ def plot_boundaries( position of plane in z direction, only one of x, y, z must be specified to define plane. ax : matplotlib.axes._subplots.Axes = None Matplotlib axes to plot on, if not specified, one is created. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) **kwargs Optional keyword arguments passed to the matplotlib ``LineCollection``. For details on accepted values, refer to @@ -519,6 +568,7 @@ def plot_structures( hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, fill: bool = True, + transpose: bool = False, ) -> Ax: """Plot each of simulation's structures on a plane defined by one nonzero x,y,z coordinate. @@ -538,6 +588,9 @@ def plot_structures( The z range if plotting on xz or yz planes, y plane if plotting on xy plane. fill : bool = True Whether to fill structures with color or just draw outlines. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) + Returns ------- matplotlib.axes._subplots.Axes @@ -545,11 +598,11 @@ def plot_structures( """ hlim_new, vlim_new = Scene._get_plot_lims( - bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim + bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose ) return self.scene.plot_structures( - x=x, y=y, z=z, ax=ax, hlim=hlim_new, vlim=vlim_new, fill=fill + x=x, y=y, z=z, ax=ax, hlim=hlim_new, vlim=vlim_new, fill=fill, transpose=transpose ) @equal_aspect @@ -566,6 +619,7 @@ def plot_structures_eps( ax: Ax = None, hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, + transpose: bool = False, ) -> Ax: """Plot each of simulation's structures on a plane defined by one nonzero x,y,z coordinate. The permittivity is plotted in grayscale based on its value at the specified frequency. @@ -595,6 +649,8 @@ def plot_structures_eps( The x range if plotting on xy or xz planes, y range if plotting on yz plane. vlim : Tuple[float, float] = None The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -603,7 +659,7 @@ def plot_structures_eps( """ hlim, vlim = Scene._get_plot_lims( - bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim + bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose ) return self.scene.plot_structures_eps( @@ -617,6 +673,7 @@ def plot_structures_eps( hlim=hlim, vlim=vlim, reverse=reverse, + transpose=transpose, ) @equal_aspect @@ -632,9 +689,10 @@ def plot_structures_heat_conductivity( ax: Ax = None, hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, + transpose: bool = False, ) -> Ax: """Plot each of simulation's structures on a plane defined by one nonzero x,y,z coordinate. - The permittivity is plotted in grayscale based on its value at the specified frequency. + The conductivity is plotted in grayscale based on its value. Parameters ---------- @@ -644,14 +702,11 @@ def plot_structures_heat_conductivity( position of plane in y direction, only one of x, y, z must be specified to define plane. z : float = None position of plane in z direction, only one of x, y, z must be specified to define plane. - freq : float = None - Frequency to evaluate the relative permittivity of all mediums. - If not specified, evaluates at infinite frequency. reverse : bool = False - If ``False``, the highest permittivity is plotted in black. + If ``False``, the highest conductivity is plotted in black. If ``True``, it is plotteed in white (suitable for black backgrounds). cbar : bool = True - Whether to plot a colorbar for the relative permittivity. + Whether to plot a colorbar for the relative conductivity. alpha : float = None Opacity of the structures being plotted. Defaults to the structure default alpha. @@ -661,6 +716,9 @@ def plot_structures_heat_conductivity( The x range if plotting on xy or xz planes, y range if plotting on yz plane. vlim : Tuple[float, float] = None The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default + lexicographic axis order.) Returns ------- @@ -669,7 +727,7 @@ def plot_structures_heat_conductivity( """ hlim, vlim = Scene._get_plot_lims( - bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim + bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose ) return self.scene.plot_structures_heat_conductivity( @@ -682,6 +740,7 @@ def plot_structures_heat_conductivity( hlim=hlim, vlim=vlim, reverse=reverse, + transpose=transpose, ) @classmethod diff --git a/tidy3d/components/eme/simulation.py b/tidy3d/components/eme/simulation.py index 7ddaff66b..d3e14d19d 100644 --- a/tidy3d/components/eme/simulation.py +++ b/tidy3d/components/eme/simulation.py @@ -306,6 +306,7 @@ def plot_eme_ports( ax: Ax = None, hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, + transpose: bool = False, **kwargs, ) -> Ax: """Plot the EME ports.""" @@ -315,15 +316,19 @@ def plot_eme_ports( rmax = self.geometry.bounds[1][self.axis] ports = np.array([rmin + self.port_offsets[0], rmax - self.port_offsets[1]]) axis, _ = self.parse_xyz_kwargs(x=x, y=y, z=z) - _, (axis_x, axis_y) = self.pop_axis([0, 1, 2], axis=axis) + _, (axis_x, axis_y) = self.pop_axis_and_swap([0, 1, 2], axis=axis, transpose=transpose) boundaries_x = [] boundaries_y = [] if axis_x == self.axis: boundaries_x = ports if axis_y == self.axis: boundaries_y = ports - _, (xmin, ymin) = self.pop_axis(self.simulation_bounds[0], axis=axis) - _, (xmax, ymax) = self.pop_axis(self.simulation_bounds[1], axis=axis) + _, (xmin, ymin) = self.pop_axis_and_swap( + self.simulation_bounds[0], axis=axis, transpose=transpose + ) + _, (xmax, ymax) = self.pop_axis_and_swap( + self.simulation_bounds[1], axis=axis, transpose=transpose + ) segs_x = [((bound, ymin), (bound, ymax)) for bound in boundaries_x] line_segments_x = mpl.collections.LineCollection(segs_x, **kwargs) segs_y = [((xmin, bound), (xmax, bound)) for bound in boundaries_y] @@ -334,7 +339,14 @@ def plot_eme_ports( ax.add_collection(line_segments_y) ax = Scene._set_plot_bounds( - bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + bounds=self.simulation_bounds, + ax=ax, + x=x, + y=y, + z=z, + hlim=hlim, + vlim=vlim, + transpose=transpose, ) return ax @@ -350,6 +362,7 @@ def plot_eme_subgrid_boundaries( ax: Ax = None, hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, + transpose: bool = False, **kwargs, ) -> Ax: """Plot the EME subgrid boundaries. @@ -363,15 +376,19 @@ def plot_eme_subgrid_boundaries( subgrid_boundaries = np.array(eme_grid_spec.subgrid_boundaries) subgrids = eme_grid_spec.subgrids axis, _ = self.parse_xyz_kwargs(x=x, y=y, z=z) - _, (axis_x, axis_y) = self.pop_axis([0, 1, 2], axis=axis) + _, (axis_x, axis_y) = self.pop_axis_and_swap([0, 1, 2], axis=axis, transpose=transpose) boundaries_x = [] boundaries_y = [] if axis_x == self.axis: boundaries_x = subgrid_boundaries if axis_y == self.axis: boundaries_y = subgrid_boundaries - _, (xmin, ymin) = self.pop_axis(self.simulation_bounds[0], axis=axis) - _, (xmax, ymax) = self.pop_axis(self.simulation_bounds[1], axis=axis) + _, (xmin, ymin) = self.pop_axis_and_swap( + self.simulation_bounds[0], axis=axis, transpose=transpose + ) + _, (xmax, ymax) = self.pop_axis_and_swap( + self.simulation_bounds[1], axis=axis, transpose=transpose + ) segs_x = [((bound, ymin), (bound, ymax)) for bound in boundaries_x] line_segments_x = mpl.collections.LineCollection(segs_x, **kwargs) segs_y = [((xmin, bound), (xmax, bound)) for bound in boundaries_y] @@ -382,12 +399,27 @@ def plot_eme_subgrid_boundaries( ax.add_collection(line_segments_y) ax = Scene._set_plot_bounds( - bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + bounds=self.simulation_bounds, + ax=ax, + x=x, + y=y, + z=z, + hlim=hlim, + vlim=vlim, + transpose=transpose, ) for subgrid in subgrids: ax = self.plot_eme_subgrid_boundaries( - eme_grid_spec=subgrid, x=x, y=y, z=z, ax=ax, hlim=hlim, vlim=vlim, **kwargs + eme_grid_spec=subgrid, + x=x, + y=y, + z=z, + ax=ax, + hlim=hlim, + vlim=vlim, + transpose=transpose, + **kwargs, ) return ax @@ -402,6 +434,7 @@ def plot_eme_grid( ax: Ax = None, hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, + transpose: bool = False, **kwargs, ) -> Ax: """Plot the EME grid.""" @@ -409,15 +442,19 @@ def plot_eme_grid( kwargs.setdefault("colors", "black") cell_boundaries = self.eme_grid.boundaries axis, _ = self.parse_xyz_kwargs(x=x, y=y, z=z) - _, (axis_x, axis_y) = self.pop_axis([0, 1, 2], axis=axis) + _, (axis_x, axis_y) = self.pop_axis_and_swap([0, 1, 2], axis=axis, transpose=transpose) boundaries_x = [] boundaries_y = [] if axis_x == self.axis: boundaries_x = cell_boundaries if axis_y == self.axis: boundaries_y = cell_boundaries - _, (xmin, ymin) = self.pop_axis(self.simulation_bounds[0], axis=axis) - _, (xmax, ymax) = self.pop_axis(self.simulation_bounds[1], axis=axis) + _, (xmin, ymin) = self.pop_axis_and_swap( + self.simulation_bounds[0], axis=axis, transpose=transpose + ) + _, (xmax, ymax) = self.pop_axis_and_swap( + self.simulation_bounds[1], axis=axis, transpose=transpose + ) segs_x = [((bound, ymin), (bound, ymax)) for bound in boundaries_x] line_segments_x = mpl.collections.LineCollection(segs_x, **kwargs) segs_y = [((xmin, bound), (xmax, bound)) for bound in boundaries_y] @@ -428,7 +465,14 @@ def plot_eme_grid( ax.add_collection(line_segments_y) ax = Scene._set_plot_bounds( - bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + bounds=self.simulation_bounds, + ax=ax, + x=x, + y=y, + z=z, + hlim=hlim, + vlim=vlim, + transpose=transpose, ) return ax @@ -445,6 +489,7 @@ def plot( monitor_alpha: Optional[float] = None, hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, + transpose: bool = False, **patch_kwargs, ) -> Ax: """Plot each of simulation's components on a plane defined by one nonzero x,y,z coordinate. @@ -467,6 +512,8 @@ def plot( The x range if plotting on xy or xz planes, y range if plotting on yz plane. vlim : Tuple[float, float] = None The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -475,23 +522,43 @@ def plot( """ hlim, vlim = Scene._get_plot_lims( - bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim + bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose ) - ax = self.scene.plot_structures(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) - ax = self.plot_sources(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=source_alpha) - ax = self.plot_monitors(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=monitor_alpha) + ax = self.scene.plot_structures( + ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose + ) + ax = self.plot_sources( + ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=source_alpha, transpose=transpose + ) + ax = self.plot_monitors( + ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=monitor_alpha, transpose=transpose + ) ax = Scene._set_plot_bounds( - bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + bounds=self.simulation_bounds, + ax=ax, + x=x, + y=y, + z=z, + hlim=hlim, + vlim=vlim, + transpose=transpose, ) - ax = self.plot_boundaries(ax=ax, x=x, y=y, z=z) - ax = self.plot_symmetries(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + ax = self.plot_boundaries(ax=ax, x=x, y=y, z=z, transpose=transpose) + ax = self.plot_symmetries(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose) - ax = self.plot_eme_grid(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + ax = self.plot_eme_grid(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose) ax = self.plot_eme_subgrid_boundaries( - eme_grid_spec=self.eme_grid_spec, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + eme_grid_spec=self.eme_grid_spec, + ax=ax, + x=x, + y=y, + z=z, + hlim=hlim, + vlim=vlim, + transpose=transpose, ) - ax = self.plot_eme_ports(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + ax = self.plot_eme_ports(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose) return ax @cached_property diff --git a/tidy3d/components/geometry/base.py b/tidy3d/components/geometry/base.py index 4d724d558..e52a2a112 100644 --- a/tidy3d/components/geometry/base.py +++ b/tidy3d/components/geometry/base.py @@ -236,7 +236,11 @@ def intersections_tilted_plane( """ def intersections_plane( - self, x: Optional[float] = None, y: Optional[float] = None, z: Optional[float] = None + self, + x: Optional[float] = None, + y: Optional[float] = None, + z: Optional[float] = None, + transpose: bool = False, ) -> list[Shapely]: """Returns list of shapely geometries at plane specified by one non-None value of x,y,z. @@ -248,6 +252,9 @@ def intersections_plane( Position of plane in y direction, only one of x,y,z can be specified to define plane. z : float = None Position of plane in z direction, only one of x,y,z can be specified to define plane. + transpose : bool = False + Optional: Swap the coordinates in the plane. (This overrides the + default lexicographic axis order.) Returns ------- @@ -261,14 +268,22 @@ def intersections_plane( normal = self.unpop_axis(1, (0, 0), axis=axis) to_2D = np.eye(4) if axis != 2: - last, indices = self.pop_axis((0, 1, 2), axis) + last, indices = self.pop_axis_and_swap((0, 1, 2), axis, transpose=transpose) to_2D = to_2D[[*list(indices), last, 3]] return self.intersections_tilted_plane(normal, origin, to_2D) - def intersections_2dbox(self, plane: Box) -> list[Shapely]: + def intersections_2dbox(self, plane: Box, transpose: bool = False) -> list[Shapely]: """Returns list of shapely geometries representing the intersections of the geometry with a 2D box. + Parameters + ---------- + plane : Box + Plane specification. + transpose : bool = False + Optional: Swap the coordinates in the plane. (This overrides the + default lexicographic axis order.) + Returns ------- List[shapely.geometry.base.BaseGeometry] @@ -279,7 +294,7 @@ def intersections_2dbox(self, plane: Box) -> list[Shapely]: "'intersections_2dbox()' is deprecated and will be removed in the future. " "Use 'plane.intersections_with(...)' for the same functionality." ) - return plane.intersections_with(self) + return plane.intersections_with(self, transpose=transpose) def intersects( self, other, strict_inequality: tuple[bool, bool, bool] = [False, False, False] @@ -439,13 +454,18 @@ def zero_dims(self) -> list[Axis]: zero_dims.append(dim) return zero_dims - def _pop_bounds(self, axis: Axis) -> tuple[Coordinate2D, tuple[Coordinate2D, Coordinate2D]]: + def _pop_bounds( + self, axis: Axis, transpose: bool = False + ) -> tuple[Coordinate2D, tuple[Coordinate2D, Coordinate2D]]: """Returns min and max bounds in plane normal to and tangential to ``axis``. Parameters ---------- axis : int Integer index into 'xyz' (0,1,2). + transpose : bool = False + Optional: Swap the coordinates in the plane. (This overrides the + default lexicographic axis order.) Returns ------- @@ -454,8 +474,8 @@ def _pop_bounds(self, axis: Axis) -> tuple[Coordinate2D, tuple[Coordinate2D, Coo Packed as ``(zmin, zmax), ((xmin, ymin), (xmax, ymax))``. """ b_min, b_max = self.bounds - zmin, (xmin, ymin) = self.pop_axis(b_min, axis=axis) - zmax, (xmax, ymax) = self.pop_axis(b_max, axis=axis) + zmin, (xmin, ymin) = self.pop_axis_and_swap(b_min, axis=axis, transpose=transpose) + zmax, (xmax, ymax) = self.pop_axis_and_swap(b_max, axis=axis, transpose=transpose) return (zmin, zmax), ((xmin, ymin), (xmax, ymax)) @staticmethod @@ -496,6 +516,7 @@ def plot( ax: Ax = None, plot_length_units: LengthUnit = None, viz_spec: VisualizationSpec = None, + transpose: bool = False, **patch_kwargs, ) -> Ax: """Plot geometry cross section at single (x,y,z) coordinate. @@ -514,6 +535,9 @@ def plot( Specify units to use for axis labels, tick labels, and the title. viz_spec : VisualizationSpec = None Plotting parameters associated with a medium to use instead of defaults. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default + lexicographic axis order.) **patch_kwargs Optional keyword arguments passed to the matplotlib patch plotting of structure. For details on accepted values, refer to @@ -524,10 +548,9 @@ def plot( matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ - # find shapes that intersect self at plane axis, position = self.parse_xyz_kwargs(x=x, y=y, z=z) - shapes_intersect = self.intersections_plane(x=x, y=y, z=z) + shapes_intersect = self.intersections_plane(x=x, y=y, z=z, transpose=transpose) plot_params = self.plot_params if viz_spec is not None: @@ -539,10 +562,12 @@ def plot( ax = self.plot_shape(shape, plot_params=plot_params, ax=ax) # clean up the axis display - ax = self.add_ax_lims(axis=axis, ax=ax) + ax = self.add_ax_lims(axis=axis, ax=ax, transpose=transpose) ax.set_aspect("equal") # Add the default axis labels, tick labels, and title - ax = Box.add_ax_labels_and_title(ax=ax, x=x, y=y, z=z, plot_length_units=plot_length_units) + ax = Box.add_ax_labels_and_title( + ax=ax, x=x, y=y, z=z, plot_length_units=plot_length_units, transpose=transpose + ) return ax def plot_shape(self, shape: Shapely, plot_params: PlotParams, ax: Ax) -> Ax: @@ -590,24 +615,27 @@ def _do_not_intersect(bounds_a, bounds_b, shape_a, shape_b): return False @staticmethod - def _get_plot_labels(axis: Axis) -> tuple[str, str]: + def _get_plot_labels(axis: Axis, transpose: bool = False) -> tuple[str, str]: """Returns planar coordinate x and y axis labels for cross section plots. Parameters ---------- axis : int Integer index into 'xyz' (0,1,2). + transpose : bool = False + Optional: Swap horizontal and vertical plot labels. + (This overrides the default lexicographic axis order.) Returns ------- str, str Labels of plot, packaged as ``(xlabel, ylabel)``. """ - _, (xlabel, ylabel) = Geometry.pop_axis("xyz", axis=axis) + _, (xlabel, ylabel) = Geometry.pop_axis_and_swap("xyz", axis=axis, transpose=transpose) return xlabel, ylabel def _get_plot_limits( - self, axis: Axis, buffer: float = PLOT_BUFFER + self, axis: Axis, buffer: float = PLOT_BUFFER, transpose: bool = False ) -> tuple[Coordinate2D, Coordinate2D]: """Gets planar coordinate limits for cross section plots. @@ -617,17 +645,22 @@ def _get_plot_limits( Integer index into 'xyz' (0,1,2). buffer : float = 0.3 Amount of space to add around the limits on the + and - sides. + transpose : bool = False + Optional: Swap horizontal and vertical axis limits. + (This overrides the default lexicographic axis order.) Returns ------- Tuple[float, float], Tuple[float, float] The x and y plot limits, packed as ``(xmin, xmax), (ymin, ymax)``. """ - _, ((xmin, ymin), (xmax, ymax)) = self._pop_bounds(axis=axis) + _, ((xmin, ymin), (xmax, ymax)) = self._pop_bounds(axis=axis, transpose=transpose) return (xmin - buffer, xmax + buffer), (ymin - buffer, ymax + buffer) - def add_ax_lims(self, axis: Axis, ax: Ax, buffer: float = PLOT_BUFFER) -> Ax: - """Sets the x,y limits based on ``self.bounds``. + def add_ax_lims( + self, axis: Axis, ax: Ax, buffer: float = PLOT_BUFFER, transpose: bool = False + ) -> Ax: + """Sets the horizontal and vertical axis limits based on ``self.bounds``. Parameters ---------- @@ -637,13 +670,18 @@ def add_ax_lims(self, axis: Axis, ax: Ax, buffer: float = PLOT_BUFFER) -> Ax: Matplotlib axes to add labels and limits on. buffer : float = 0.3 Amount of space to place around the limits on the + and - sides. + transpose : bool = False + Optional: Swap horizontal and vertical axis limits. + (This overrides the default lexicographic axis order.) Returns ------- matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ - (xmin, xmax), (ymin, ymax) = self._get_plot_limits(axis=axis, buffer=buffer) + (xmin, xmax), (ymin, ymax) = self._get_plot_limits( + axis=axis, buffer=buffer, transpose=transpose + ) # note: axes limits dont like inf values, so we need to evaluate them first if present xmin, xmax, ymin, ymax = self._evaluate_inf((xmin, xmax, ymin, ymax)) @@ -659,6 +697,7 @@ def add_ax_labels_and_title( y: Optional[float] = None, z: Optional[float] = None, plot_length_units: LengthUnit = None, + transpose: bool = False, ) -> Ax: """Sets the axis labels, tick labels, and title based on ``axis`` and an optional ``plot_length_units`` argument. @@ -676,6 +715,9 @@ def add_ax_labels_and_title( plot_length_units : LengthUnit = None When set to a supported ``LengthUnit``, plots will be produced with annotated axes and title with the proper units. + transpose : bool = False + Optional: Swap horizontal and vertical axis labels. + (This overrides the default lexicographic axis order.) Returns ------- @@ -683,7 +725,7 @@ def add_ax_labels_and_title( The supplied matplotlib axes. """ axis, position = Box.parse_xyz_kwargs(x=x, y=y, z=z) - axis_labels = Box._get_plot_labels(axis) + axis_labels = Box._get_plot_labels(axis, transpose=transpose) ax = set_default_labels_and_title( axis_labels=axis_labels, axis=axis, @@ -722,7 +764,10 @@ def evaluate_inf_shape(shape: Shapely) -> Shapely: return shape @staticmethod - def pop_axis(coord: tuple[Any, Any, Any], axis: int) -> tuple[Any, tuple[Any, Any]]: + def pop_axis( + coord: tuple[Any, Any, Any], + axis: int, + ) -> tuple[Any, tuple[Any, Any]]: """Separates coordinate at ``axis`` index from coordinates on the plane tangent to ``axis``. Parameters @@ -744,7 +789,45 @@ def pop_axis(coord: tuple[Any, Any, Any], axis: int) -> tuple[Any, tuple[Any, An return axis_val, tuple(plane_vals) @staticmethod - def unpop_axis(ax_coord: Any, plane_coords: tuple[Any, Any], axis: int) -> tuple[Any, Any, Any]: + def pop_axis_and_swap( + coord: tuple[Any, Any, Any], + axis: int, + transpose: bool = False, + ) -> tuple[Any, tuple[Any, Any]]: + """ + ``pop_axis_and_swap()`` is identical to ``pop_axis()``, except that it accepts an + additional ``transpose`` argument which reverses the output order. Examples: + + ``pop_axis_and_swap(("x", "y", "z"), 1, transpose=False)`` -> ``("y", ("x", "z"))`` + ``pop_axis_and_swap(("x", "y", "z"), 1, transpose=True)`` -> ``("y", ("z", "x"))`` + + Parameters + ---------- + coord : Tuple[Any, Any, Any] + Tuple of three values in original coordinate system. + axis : int + Integer index into 'xyz' (0,1,2). + transpose : bool = False + Optional: Swap the order of the data from the two remaining axes in the output tuple. + + Returns + ------- + Any, Tuple[Any, Any] + The input coordinates are separated into the one along the axis provided + and the two on the planar coordinates, + like ``axis_coord, (planar_coord1, planar_coord2)``. + """ + axis_val, plane_vals = Geometry.pop_axis(coord, axis) + if transpose: + return axis_val, (plane_vals[1], plane_vals[0]) + return axis_val, plane_vals + + @staticmethod + def unpop_axis( + ax_coord: Any, + plane_coords: tuple[Any, Any], + axis: int, + ) -> tuple[Any, Any, Any]: """Combine coordinate along axis with coordinates on the plane tangent to the axis. Parameters @@ -765,6 +848,46 @@ def unpop_axis(ax_coord: Any, plane_coords: tuple[Any, Any], axis: int) -> tuple coords.insert(axis, ax_coord) return tuple(coords) + @staticmethod + def unpop_axis_and_swap( + ax_coord: Any, + plane_coords: tuple[Any, Any], + axis: int, + transpose: bool = False, + ) -> tuple[Any, Any, Any]: + """ + ``unpop_axis_and_swap()`` is identical to ``unpop_axis()``, except that + it accepts an additional ``transpose`` argument which reverses the order of + ``plane_coords`` before sending them to ``unpop_axis()``. For example: + + ``unpop_axis_and_swap("y", ("x", "z"), 1, transpose=False)`` --> ``("x", "y", "z")`` + ``unpop_axis_and_swap("y", ("x", "z"), 1, transpose=True)`` --> ``("z", "y", "x")`` + + This function is the inverse of ``pop_axis_and_swap()``. For example: + ``unpop_axis_and_swap("y", ("z", "x"), 1, transpose=True)`` --> ``("x", "y", "z")`` + + Parameters + ---------- + ax_coord : Any + Value along axis direction. + plane_coords : Tuple[Any, Any] + Values along ordered planar directions. + axis : int + Integer index into 'xyz' (0,1,2). + transpose : bool = False + Optional: Swap the order of the entries in plane_coords[]. + (This overrides the default lexicographic axis order.) + + Returns + ------- + Tuple[Any, Any, Any] + The three values in the xyz coordinate system. + """ + coords = plane_coords + if transpose: + coords = (coords[1], coords[0]) + return Geometry.unpop_axis(ax_coord, coords, axis) + @staticmethod def parse_xyz_kwargs(**xyz) -> tuple[Axis, float]: """Turns x,y,z kwargs into index of the normal axis and position along that axis. @@ -1666,7 +1789,11 @@ def finite_length_axis(self) -> float: return min(self.length_axis, LARGE_NUMBER) def intersections_plane( - self, x: Optional[float] = None, y: Optional[float] = None, z: Optional[float] = None + self, + x: Optional[float] = None, + y: Optional[float] = None, + z: Optional[float] = None, + transpose: bool = False, ): """Returns shapely geometry at plane specified by one non None value of x,y,z. @@ -1678,6 +1805,9 @@ def intersections_plane( Position of plane in y direction, only one of x,y,z can be specified to define plane. z : float Position of plane in z direction, only one of x,y,z can be specified to define plane. + transpose : bool = False + Optional: Swap the coordinates in the plane before calculating intersections. + (This overrides the default lexicographic axis order.) Returns ------- @@ -1690,17 +1820,20 @@ def intersections_plane( if not self.intersects_axis_position(axis, position): return [] if axis == self.axis: - return self._intersections_normal(position) - return self._intersections_side(position, axis) + return self._intersections_normal(position, transpose=transpose) + return self._intersections_side(position, axis, transpose=transpose) @abstractmethod - def _intersections_normal(self, z: float) -> list: + def _intersections_normal(self, z: float, transpose: bool = False) -> list: """Find shapely geometries intersecting planar geometry with axis normal to slab. Parameters ---------- z : float Position along the axis normal to slab + transpose : bool = False + Optional: Swap the coordinates in the plane before calculating intersections. + (This overrides the default lexicographic axis order.) Returns ------- @@ -1711,7 +1844,7 @@ def _intersections_normal(self, z: float) -> list: """ @abstractmethod - def _intersections_side(self, position: float, axis: Axis) -> list: + def _intersections_side(self, position: float, axis: Axis, transpose: bool = False) -> list: """Find shapely geometries intersecting planar geometry with axis orthogonal to plane. Parameters @@ -1720,6 +1853,9 @@ def _intersections_side(self, position: float, axis: Axis) -> list: Position along axis. axis : int Integer index into 'xyz' (0,1,2). + transpose : bool = False + Optional: Swap the coordinates in the perpendicular plane before calculating + intersections. (This overrides the default lexicographic axis order.) Returns ------- @@ -1746,7 +1882,9 @@ def _order_axis(self, axis: int) -> int: axis_index.insert(self.axis, 2) return axis_index[axis] - def _order_by_axis(self, plane_val: Any, axis_val: Any, axis: int) -> tuple[Any, Any]: + def _order_by_axis( + self, plane_val: Any, axis_val: Any, axis: int, transpose: bool = False + ) -> tuple[Any, Any]: """Orders a value in the plane and value along axis in correct (x,y) order for plotting. Note: sometimes if axis=1 and we compute cross section values orthogonal to axis, they can either be x or y in the plots. @@ -1760,6 +1898,8 @@ def _order_by_axis(self, plane_val: Any, axis_val: Any, axis: int) -> tuple[Any, The value in the ``axis`` coordinate. axis : int Integer index into the structure's planar axis. + transpose : bool + Optional: Swap the order for the remaining two axes (the axes not equal to axis). Returns ------- @@ -1768,7 +1908,7 @@ def _order_by_axis(self, plane_val: Any, axis_val: Any, axis: int) -> tuple[Any, """ vals = 3 * [plane_val] vals[self.axis] = axis_val - _, (val_x, val_y) = self.pop_axis(vals, axis=axis) + _, (val_x, val_y) = self.pop_axis_and_swap(vals, axis=axis, transpose=transpose) return val_x, val_y @cached_property @@ -2033,7 +2173,11 @@ def _do_intersections_tilted_plane( return path.polygons_full def intersections_plane( - self, x: Optional[float] = None, y: Optional[float] = None, z: Optional[float] = None + self, + x: Optional[float] = None, + y: Optional[float] = None, + z: Optional[float] = None, + transpose: bool = False, ): """Returns shapely geometry at plane specified by one non None value of x,y,z. @@ -2045,6 +2189,9 @@ def intersections_plane( Position of plane in y direction, only one of x,y,z can be specified to define plane. z : float = None Position of plane in z direction, only one of x,y,z can be specified to define plane. + transpose : bool = False + Optional: Swap the coordinates in the plane before calculating intersections. + (This overrides the default lexicographic axis order.) Returns ------- @@ -2056,8 +2203,8 @@ def intersections_plane( axis, position = self.parse_xyz_kwargs(x=x, y=y, z=z) if not self.intersects_axis_position(axis, position): return [] - z0, (x0, y0) = self.pop_axis(self.center, axis=axis) - Lz, (Lx, Ly) = self.pop_axis(self.size, axis=axis) + z0, (x0, y0) = self.pop_axis_and_swap(self.center, axis=axis, transpose=transpose) + Lz, (Lx, Ly) = self.pop_axis_and_swap(self.size, axis=axis, transpose=transpose) dz = np.abs(z0 - position) if dz > Lz / 2 + fp_eps: return [] @@ -2102,7 +2249,7 @@ def inside( dist_z = np.abs(z - z0) return (dist_x <= Lx / 2) * (dist_y <= Ly / 2) * (dist_z <= Lz / 2) - def intersections_with(self, other): + def intersections_with(self, other, transpose: bool = False): """Returns list of shapely geometries representing the intersections of the geometry with this 2D box. @@ -2129,10 +2276,13 @@ def intersections_with(self, other): dim = "xyz"[normal_ind] pos = self.center[normal_ind] xyz_kwargs = {dim: pos} - shapes_plane = other.intersections_plane(**xyz_kwargs) + shapes_plane = other.intersections_plane(**xyz_kwargs, transpose=transpose) # intersect all shapes with the input self - bs_min, bs_max = (self.pop_axis(bounds, axis=normal_ind)[1] for bounds in self.bounds) + bs_min, bs_max = ( + self.pop_axis_and_swap(bounds, axis=normal_ind, transpose=transpose)[1] + for bounds in self.bounds + ) shapely_box = self.make_shapely_box(bs_min[0], bs_min[1], bs_max[0], bs_max[1]) shapely_box = Geometry.evaluate_inf_shape(shapely_box) @@ -2200,6 +2350,7 @@ def _plot_arrow( both_dirs: bool = False, ax: Ax = None, arrow_base: Coordinate = None, + transpose: bool = False, ) -> Ax: """Adds an arrow to the axis if with options if certain conditions met. @@ -2225,6 +2376,9 @@ def _plot_arrow( If True, plots an arrow pointing in direction and one in -direction. arrow_base : :class:`.Coordinate` = None Custom base of the arrow. Uses the geometry's center if not provided. + transpose : bool = False + Swap horizontal and vertical axes. + (This overrides the default lexicographic axis order.) Returns ------- @@ -2233,11 +2387,13 @@ def _plot_arrow( """ plot_axis, _ = self.parse_xyz_kwargs(x=x, y=y, z=z) - _, (dx, dy) = self.pop_axis(direction, axis=plot_axis) + _, (dx, dy) = self.pop_axis_and_swap(direction, axis=plot_axis, transpose=transpose) # conditions to check to determine whether to plot arrow, taking into account the # possibility of a custom arrow base - arrow_intersecting_plane = len(self.intersections_plane(x=x, y=y, z=z)) > 0 + arrow_intersecting_plane = ( + len(self.intersections_plane(x=x, y=y, z=z, transpose=transpose)) > 0 + ) center = self.center if arrow_base: arrow_intersecting_plane = arrow_intersecting_plane and any( @@ -2245,12 +2401,12 @@ def _plot_arrow( ) center = arrow_base - _, (dx, dy) = self.pop_axis(direction, axis=plot_axis) + _, (dx, dy) = self.pop_axis_and_swap(direction, axis=plot_axis, transpose=transpose) components_in_plane = any(not np.isclose(component, 0) for component in (dx, dy)) # plot if arrow in plotting plane and some non-zero component can be displayed. if arrow_intersecting_plane and components_in_plane: - _, (x0, y0) = self.pop_axis(center, axis=plot_axis) + _, (x0, y0) = self.pop_axis_and_swap(center, axis=plot_axis, transpose=transpose) # Reasonable value for temporary arrow size. The correct size and direction # have to be calculated after all transforms have been set. That is why we @@ -2932,7 +3088,11 @@ def intersections_tilted_plane( return ClipOperation.to_polygon_list(self._shapely_operation(geom_a, geom_b)) def intersections_plane( - self, x: Optional[float] = None, y: Optional[float] = None, z: Optional[float] = None + self, + x: Optional[float] = None, + y: Optional[float] = None, + z: Optional[float] = None, + transpose: bool = False, ) -> list[Shapely]: """Returns list of shapely geometries at plane specified by one non-None value of x,y,z. @@ -2944,6 +3104,9 @@ def intersections_plane( Position of plane in y direction, only one of x,y,z can be specified to define plane. z : float = None Position of plane in z direction, only one of x,y,z can be specified to define plane. + transpose : bool = False + Optional: Swap the coordinates in the plane before calculating intersections. + (This overrides the default lexicographic axis order.) Returns ------- @@ -2952,8 +3115,8 @@ def intersections_plane( For more details refer to `Shapely's Documentaton `_. """ - a = self.geometry_a.intersections_plane(x, y, z) - b = self.geometry_b.intersections_plane(x, y, z) + a = self.geometry_a.intersections_plane(x, y, z, transpose=transpose) + b = self.geometry_b.intersections_plane(x, y, z, transpose=transpose) geom_a = shapely.unary_union([Geometry.evaluate_inf_shape(g) for g in a]) geom_b = shapely.unary_union([Geometry.evaluate_inf_shape(g) for g in b]) return ClipOperation.to_polygon_list(self._shapely_operation(geom_a, geom_b)) @@ -3140,7 +3303,11 @@ def intersections_tilted_plane( ] def intersections_plane( - self, x: Optional[float] = None, y: Optional[float] = None, z: Optional[float] = None + self, + x: Optional[float] = None, + y: Optional[float] = None, + z: Optional[float] = None, + transpose: bool = False, ) -> list[Shapely]: """Returns list of shapely geometries at plane specified by one non-None value of x,y,z. @@ -3152,6 +3319,9 @@ def intersections_plane( Position of plane in y direction, only one of x,y,z can be specified to define plane. z : float = None Position of plane in z direction, only one of x,y,z can be specified to define plane. + transpose : bool = False + Optional: Swap the coordinates in the plane before calculating intersections. + (This overrides the default lexicographic axis order.) Returns ------- @@ -3165,7 +3335,7 @@ def intersections_plane( return [ intersection for geometry in self.geometries - for intersection in geometry.intersections_plane(x=x, y=y, z=z) + for intersection in geometry.intersections_plane(x=x, y=y, z=z, transpose=transpose) ] def intersects_axis_position(self, axis: float, position: float) -> bool: diff --git a/tidy3d/components/geometry/mesh.py b/tidy3d/components/geometry/mesh.py index a05c887c7..413bf9fa6 100644 --- a/tidy3d/components/geometry/mesh.py +++ b/tidy3d/components/geometry/mesh.py @@ -554,7 +554,11 @@ def intersections_tilted_plane( return path.polygons_full def intersections_plane( - self, x: Optional[float] = None, y: Optional[float] = None, z: Optional[float] = None + self, + x: Optional[float] = None, + y: Optional[float] = None, + z: Optional[float] = None, + transpose: bool = False, ) -> list[Shapely]: """Returns list of shapely geometries at plane specified by one non-None value of x,y,z. @@ -566,6 +570,8 @@ def intersections_plane( Position of plane in y direction, only one of x,y,z can be specified to define plane. z : float = None Position of plane in z direction, only one of x,y,z can be specified to define plane. + transpose : bool = False + Optional: Swap the coordinates in the plane before calculating intersections. Returns ------- @@ -600,7 +606,9 @@ def intersections_plane( # permute so normal is aligned with z axis # and (y, z), (x, z), resp. (x, y) are aligned with (x, y) identity = np.eye(3) - permutation = self.unpop_axis(identity[2], identity[0:2], axis=axis) + permutation = self.unpop_axis_and_swap( + identity[2], identity[0:2], axis=axis, transpose=transpose + ) mapping[:3, :3] = np.array(permutation).T section2d, _ = section.to_planar(to_2D=mapping) @@ -620,7 +628,7 @@ def intersections_plane( "Using bounding box instead." ) log.warning(f"Error encountered: {e}") - return self.bounding_box.intersections_plane(x=x, y=y, z=z) + return self.bounding_box.intersections_plane(x=x, y=y, z=z, transpose=transpose) def inside( self, x: np.ndarray[float], y: np.ndarray[float], z: np.ndarray[float] diff --git a/tidy3d/components/geometry/polyslab.py b/tidy3d/components/geometry/polyslab.py index 7d7c1e6f0..eefda2d06 100644 --- a/tidy3d/components/geometry/polyslab.py +++ b/tidy3d/components/geometry/polyslab.py @@ -629,13 +629,15 @@ def _do_intersections_tilted_plane( path, _ = section.to_planar(to_2D=to_2D) return path.polygons_full - def _intersections_normal(self, z: float): + def _intersections_normal(self, z: float, transpose: bool = False): """Find shapely geometries intersecting planar geometry with axis normal to slab. Parameters ---------- z : float Position along the axis normal to slab. + transpose : bool = False + Optional: Swap the coordinates in the plane before calculating intersections. Returns ------- @@ -645,15 +647,20 @@ def _intersections_normal(self, z: float): `Shapely's Documentation `_. """ if math.isclose(self.sidewall_angle, 0): - return [self.make_shapely_polygon(self.reference_polygon)] - + vertices = self.reference_polygon + if transpose: + vertices = vertices[:, ::-1] # swap column 0 (x coords) with column 1 (y coords) + return [self.make_shapely_polygon(vertices)] z0 = self.center_axis z_local = z - z0 # distance to the middle dist = -z_local * self._tanq vertices_z = self._shift_vertices(self.middle_polygon, dist)[0] - return [self.make_shapely_polygon(vertices_z)] + vertices = vertices_z + if transpose: + vertices = vertices[:, ::-1] # swap column 0 (x coords) with column 1 (y coords) + return [self.make_shapely_polygon(vertices)] - def _intersections_side(self, position, axis) -> list: + def _intersections_side(self, position, axis, transpose: bool = False) -> list: """Find shapely geometries intersecting planar geometry with axis orthogonal to slab. For slanted polyslab, the procedure is as follows, @@ -678,6 +685,8 @@ def _intersections_side(self, position, axis) -> list: Position along ``axis``. axis : int Integer index into 'xyz' (0,1,2). + transpose : bool = False + Optional: Swap the coordinates in the perpendicular plane before calculating intersections. Returns ------- @@ -692,6 +701,7 @@ def _intersections_side(self, position, axis) -> list: z_base = z0 - self.finite_length_axis / 2 axis_ordered = self._order_axis(axis) + height_list = self._find_intersecting_height(position, axis_ordered) polys = [] @@ -723,8 +733,12 @@ def _intersections_side(self, position, axis) -> list: for y_index in range(len(ints_y) // 2): y_min = ints_y[2 * y_index] y_max = ints_y[2 * y_index + 1] - minx, miny = self._order_by_axis(plane_val=y_min, axis_val=z_min, axis=axis) - maxx, maxy = self._order_by_axis(plane_val=y_max, axis_val=z_max, axis=axis) + minx, miny = self._order_by_axis( + plane_val=y_min, axis_val=z_min, axis=axis, transpose=transpose + ) + maxx, maxy = self._order_by_axis( + plane_val=y_max, axis_val=z_max, axis=axis, transpose=transpose + ) if math.isclose(self.sidewall_angle, 0): polys.append(self.make_shapely_box(minx, miny, maxx, maxy)) @@ -738,13 +752,17 @@ def _intersections_side(self, position, axis) -> list: dy_min = h_length * np.tan(angle_min) dy_max = h_length * np.tan(angle_max) - x1, y1 = self._order_by_axis(plane_val=y_min, axis_val=z_min, axis=axis) - x2, y2 = self._order_by_axis(plane_val=y_max, axis_val=z_min, axis=axis) + x1, y1 = self._order_by_axis( + plane_val=y_min, axis_val=z_min, axis=axis, transpose=transpose + ) + x2, y2 = self._order_by_axis( + plane_val=y_max, axis_val=z_min, axis=axis, transpose=transpose + ) x3, y3 = self._order_by_axis( - plane_val=y_max - dy_max, axis_val=z_max, axis=axis + plane_val=y_max - dy_max, axis_val=z_max, axis=axis, transpose=transpose ) x4, y4 = self._order_by_axis( - plane_val=y_min + dy_min, axis_val=z_max, axis=axis + plane_val=y_min + dy_min, axis_val=z_max, axis=axis, transpose=transpose ) vertices = ((x1, y1), (x2, y2), (x3, y3), (x4, y4)) polys.append(self.make_shapely_polygon(vertices).buffer(0)) diff --git a/tidy3d/components/geometry/primitives.py b/tidy3d/components/geometry/primitives.py index 9e13cbe5f..61468f205 100644 --- a/tidy3d/components/geometry/primitives.py +++ b/tidy3d/components/geometry/primitives.py @@ -112,7 +112,11 @@ def intersections_tilted_plane( return [shapely.Polygon(vertices[:, :2])] def intersections_plane( - self, x: Optional[float] = None, y: Optional[float] = None, z: Optional[float] = None + self, + x: Optional[float] = None, + y: Optional[float] = None, + z: Optional[float] = None, + transpose: bool = False, ): """Returns shapely geometry at plane specified by one non None value of x,y,z. @@ -124,6 +128,8 @@ def intersections_plane( Position of plane in x direction, only one of x,y,z can be specified to define plane. z : float = None Position of plane in x direction, only one of x,y,z can be specified to define plane. + transpose : bool = False + Optional: Swap the coordinates in the plane normal to the axis before creating each shape? Returns ------- @@ -135,7 +141,7 @@ def intersections_plane( axis, position = self.parse_xyz_kwargs(x=x, y=y, z=z) if not self.intersects_axis_position(axis, position): return [] - z0, (x0, y0) = self.pop_axis(self.center, axis=axis) + z0, (x0, y0) = self.pop_axis_and_swap(self.center, axis=axis, transpose=transpose) intersect_dist = self._intersect_dist(position, z0) if not intersect_dist: return [] @@ -372,7 +378,10 @@ def _update_from_bounds(self, bounds: tuple[float, float], axis: Axis) -> Cylind @verify_packages_import(["trimesh"]) def _do_intersections_tilted_plane( - self, normal: Coordinate, origin: Coordinate, to_2D: MatrixReal4x4 + self, + normal: Coordinate, + origin: Coordinate, + to_2D: MatrixReal4x4, ) -> list[Shapely]: """Return a list of shapely geometries at the plane specified by normal and origin. @@ -467,13 +476,15 @@ def _do_intersections_tilted_plane( path, _ = section.to_planar(to_2D=to_2D) return path.polygons_full - def _intersections_normal(self, z: float): + def _intersections_normal(self, z: float, transpose: bool = False): """Find shapely geometries intersecting cylindrical geometry with axis normal to slab. Parameters ---------- z : float Position along the axis normal to slab + transpose : bool = False + Optional: Swap the coordinates in slab's plane before calculating intersections. Returns ------- @@ -490,11 +501,12 @@ def _intersections_normal(self, z: float): if radius_offset <= 0: return [] - - _, (x0, y0) = self.pop_axis(static_self.center, axis=self.axis) + _, (x0, y0) = self.pop_axis_and_swap( + static_self.center, axis=self.axis, transpose=transpose + ) return [shapely.Point(x0, y0).buffer(radius_offset, quad_segs=_N_SHAPELY_QUAD_SEGS)] - def _intersections_side(self, position, axis): + def _intersections_side(self, position, axis, transpose: bool = False): """Find shapely geometries intersecting cylindrical geometry with axis orthogonal to length. When ``sidewall_angle`` is nonzero, so that it's in fact a conical frustum or cone, the cross section can contain hyperbolic curves. This is currently approximated by a polygon @@ -506,6 +518,8 @@ def _intersections_side(self, position, axis): Position along axis direction. axis : int Integer index into 'xyz' (0, 1, 2). + transpose : bool = False + Optional: Swap the coordinates in the perpendicular plane before calculating intersections. Returns ------- @@ -530,8 +544,12 @@ def _intersections_side(self, position, axis): # the vertices on the max side of top/bottom # The two vertices are present in all scenarios. vertices_max = [ - self._local_to_global_side_cross_section([-intersect_half_length_max, 0], axis), - self._local_to_global_side_cross_section([intersect_half_length_max, 0], axis), + self._local_to_global_side_cross_section( + [-intersect_half_length_max, 0], axis, transpose=transpose + ), + self._local_to_global_side_cross_section( + [intersect_half_length_max, 0], axis, transpose=transpose + ), ] # Extending to a cone, the maximal height of the cone @@ -555,7 +573,9 @@ def _intersections_side(self, position, axis): ) for i in range(_N_SAMPLE_CURVE_SHAPELY): vertices_frustum_right.append( - self._local_to_global_side_cross_section([x_list[i], y_list[i]], axis) + self._local_to_global_side_cross_section( + [x_list[i], y_list[i]], axis, transpose=transpose + ) ) vertices_frustum_left.append( self._local_to_global_side_cross_section( @@ -564,6 +584,7 @@ def _intersections_side(self, position, axis): y_list[_N_SAMPLE_CURVE_SHAPELY - i - 1], ], axis, + transpose=transpose, ) ) @@ -574,17 +595,19 @@ def _intersections_side(self, position, axis): if intersect_half_length_min > 0: vertices_min.append( self._local_to_global_side_cross_section( - [intersect_half_length_min, self.finite_length_axis], axis + [intersect_half_length_min, self.finite_length_axis], axis, transpose=transpose ) ) vertices_min.append( self._local_to_global_side_cross_section( - [-intersect_half_length_min, self.finite_length_axis], axis + [-intersect_half_length_min, self.finite_length_axis], axis, transpose=transpose ) ) ## early termination else: - vertices_min.append(self._local_to_global_side_cross_section([0, height_max], axis)) + vertices_min.append( + self._local_to_global_side_cross_section([0, height_max], axis, transpose=transpose) + ) return [ shapely.Polygon( @@ -731,7 +754,12 @@ def _radius_z(self, z: float): return radius_middle - (z - self.center_axis) * self._tanq - def _local_to_global_side_cross_section(self, coords: list[float], axis: int) -> list[float]: + def _local_to_global_side_cross_section( + self, + coords: list[float], + axis: int, + transpose: bool = False, + ) -> list[float]: """Map a point (x,y) from local to global coordinate system in the side cross section. @@ -746,6 +774,8 @@ def _local_to_global_side_cross_section(self, coords: list[float], axis: int) -> Integer index into 'xyz' (0, 1, 2). coords : List[float, float] The value in the planar coordinate. + transpose : bool = False + Optional: Swap the coordinates in the perpendicular plane before calculating intersections. Returns ------- @@ -763,6 +793,9 @@ def _local_to_global_side_cross_section(self, coords: list[float], axis: int) -> plane_val=coords[0], axis_val=axis_sign * (-self.finite_length_axis / 2 + coords[1]), axis=axis, + transpose=transpose, + ) + _, (x_center, y_center) = self.pop_axis_and_swap( + self.center, axis=axis, transpose=transpose ) - _, (x_center, y_center) = self.pop_axis(self.center, axis=axis) return [x_center + lx_offset, y_center + ly_offset] diff --git a/tidy3d/components/geometry/utils.py b/tidy3d/components/geometry/utils.py index 8c166fdd2..a232defb8 100644 --- a/tidy3d/components/geometry/utils.py +++ b/tidy3d/components/geometry/utils.py @@ -42,6 +42,7 @@ def merging_geometries_on_plane( geometries: list[GeometryType], plane: Box, property_list: list[Any], + transpose: bool = False, ) -> list[tuple[Any, Shapely]]: """Compute list of shapes on plane. Overlaps are removed or merged depending on provided property_list. @@ -54,6 +55,8 @@ def merging_geometries_on_plane( Plane specification. property_list : List = None Property value for each structure. + transpose : bool = False + Optional: Swap the coordinates in the plane. (This overrides the default lexicographic axis order.) Returns ------- @@ -69,7 +72,7 @@ def merging_geometries_on_plane( shapes = [] for geo, prop in zip(geometries, property_list): # get list of Shapely shapes that intersect at the plane - shapes_plane = plane.intersections_with(geo) + shapes_plane = plane.intersections_with(geo, transpose=transpose) # Append each of them and their property information to the list of shapes for shape in shapes_plane: diff --git a/tidy3d/components/grid/grid.py b/tidy3d/components/grid/grid.py index 5baf3828e..0f0d7d639 100644 --- a/tidy3d/components/grid/grid.py +++ b/tidy3d/components/grid/grid.py @@ -74,7 +74,8 @@ def cell_sizes(self) -> SpatialDataArray: @cached_property def cell_size_meshgrid(self): """Returns an N-dimensional grid where N is the number of coordinate arrays that have more than one - element. Each grid element corresponds to the size of the mesh cell in N-dimensions and 1 for N=0.""" + element. Each grid element corresponds to the size of the mesh cell in N-dimensions and 1 for N=0. + """ coord_dict = self.to_dict cell_size_meshgrid = np.squeeze(np.ones(tuple(len(coord_dict[dim]) for dim in "xyz"))) diff --git a/tidy3d/components/mode/mode_solver.py b/tidy3d/components/mode/mode_solver.py index 9f5c41725..4419a848c 100644 --- a/tidy3d/components/mode/mode_solver.py +++ b/tidy3d/components/mode/mode_solver.py @@ -306,14 +306,20 @@ def normal_axis_2d(self) -> Axis2D: return idx_plane.index(self.normal_axis) @staticmethod - def _solver_symmetry(simulation: Simulation, plane: Box) -> tuple[Symmetry, Symmetry]: + def _solver_symmetry( + simulation: Simulation, + plane: Box, + transpose: bool = False, + ) -> tuple[Symmetry, Symmetry]: """Get symmetry for solver for propagation along self.normal axis.""" normal_axis = plane.size.index(0.0) mode_symmetry = list(simulation.symmetry) for dim in range(3): if simulation.center[dim] != plane.center[dim]: mode_symmetry[dim] = 0 - _, solver_sym = plane.pop_axis(mode_symmetry, axis=normal_axis) + _, solver_sym = plane.pop_axis_and_swap( + mode_symmetry, axis=normal_axis, transpose=transpose + ) return solver_sym @cached_property @@ -328,6 +334,7 @@ def _get_solver_grid( plane: Box, keep_additional_layers: bool = False, truncate_symmetry: bool = True, + transpose: bool = False, ) -> Grid: """Grid for the mode solver, not snapped to plane or simulation zero dims, and optionally corrected for symmetries. @@ -339,6 +346,8 @@ def _get_solver_grid( represent the region where custom medium data is needed for proper subpixel. truncate_symmetry : bool = True Truncate to symmetry quadrant if symmetry present. + transpose : bool = False + Swap the coordinates in the plane. (This overrides the default lexicographic axis order.) Returns ------- @@ -348,7 +357,9 @@ def _get_solver_grid( span_inds = simulation._discretize_inds_monitor(plane, colocate=False) normal_axis = plane.size.index(0.0) - solver_symmetry = cls._solver_symmetry(simulation=simulation, plane=plane) + solver_symmetry = cls._solver_symmetry( + simulation=simulation, plane=plane, transpose=transpose + ) # Remove extension along monitor normal if not keep_additional_layers: @@ -447,13 +458,19 @@ def grid_snapped(self) -> Grid: return self._grid_snapped(simulation=self.simulation, plane=self.plane) @classmethod - def _grid_snapped(cls, simulation: Simulation, plane: Box) -> Grid: + def _grid_snapped( + cls, + simulation: Simulation, + plane: Box, + transpose: bool = False, + ) -> Grid: """The solver grid snapped to the plane normal and to simulation 0-sized dims if any.""" solver_grid = cls._get_solver_grid( simulation=simulation, plane=plane, keep_additional_layers=False, truncate_symmetry=True, + transpose=transpose, ) # snap to plane center along normal direction grid_snapped = solver_grid.snap_to_box_zero_dim(plane) @@ -2086,6 +2103,7 @@ def plot_field( def plot( self, ax: Ax = None, + transpose: bool = False, **patch_kwargs, ) -> Ax: """Plot the mode plane simulation's components. @@ -2094,6 +2112,8 @@ def plot( ---------- ax : matplotlib.axes._subplots.Axes = None Matplotlib axes to plot on, if not specified, one is created. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -2109,7 +2129,7 @@ def plot( """ # Get the mode plane normal axis, center, and limits. a_center, h_lim, v_lim, _ = self._center_and_lims( - simulation=self.simulation, plane=self.plane + simulation=self.simulation, plane=self.plane, transpose=transpose ) ax = self.simulation.plot( @@ -2122,16 +2142,18 @@ def plot( monitor_alpha=0, lumped_element_alpha=0, ax=ax, + transpose=transpose, **patch_kwargs, ) - return self.plot_pml(ax=ax) + return self.plot_pml(ax=ax, transpose=transpose) def plot_eps( self, freq: Optional[float] = None, alpha: Optional[float] = None, ax: Ax = None, + transpose: bool = False, ) -> Ax: """Plot the mode plane simulation's components. The permittivity is plotted in grayscale based on its value at the specified frequency. @@ -2146,6 +2168,8 @@ def plot_eps( Defaults to the structure default alpha. ax : matplotlib.axes._subplots.Axes = None Matplotlib axes to plot on, if not specified, one is created. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -2161,7 +2185,7 @@ def plot_eps( # Get the mode plane normal axis, center, and limits. a_center, h_lim, v_lim, _ = self._center_and_lims( - simulation=self.simulation, plane=self.plane + simulation=self.simulation, plane=self.plane, transpose=transpose ) # Plot at central mode frequency if freq is not provided. @@ -2179,6 +2203,7 @@ def plot_eps( monitor_alpha=0, lumped_element_alpha=0, ax=ax, + transpose=transpose, ) def plot_structures_eps( @@ -2188,6 +2213,7 @@ def plot_structures_eps( cbar: bool = True, reverse: bool = False, ax: Ax = None, + transpose: bool = False, ) -> Ax: """Plot the mode plane simulation's components. The permittivity is plotted in grayscale based on its value at the specified frequency. @@ -2207,6 +2233,8 @@ def plot_structures_eps( If ``True``, it is plotteed in white (suitable for black backgrounds). ax : matplotlib.axes._subplots.Axes = None Matplotlib axes to plot on, if not specified, one is created. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -2222,7 +2250,7 @@ def plot_structures_eps( # Get the mode plane normal axis, center, and limits. a_center, h_lim, v_lim, _ = self._center_and_lims( - simulation=self.simulation, plane=self.plane + simulation=self.simulation, plane=self.plane, transpose=transpose ) # Plot at central mode frequency if freq is not provided. @@ -2239,11 +2267,13 @@ def plot_structures_eps( hlim=h_lim, vlim=v_lim, ax=ax, + transpose=transpose, ) def plot_grid( self, ax: Ax = None, + transpose: bool = False, **kwargs, ) -> Ax: """Plot the mode plane cell boundaries as lines. @@ -2252,6 +2282,8 @@ def plot_grid( ---------- ax : matplotlib.axes._subplots.Axes = None Matplotlib axes to plot on, if not specified, one is created. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) **kwargs Optional keyword arguments passed to the matplotlib ``LineCollection``. For details on accepted values, refer to @@ -2265,20 +2297,34 @@ def plot_grid( # Get the mode plane normal axis, center, and limits. a_center, h_lim, v_lim, _ = self._center_and_lims( - simulation=self.simulation, plane=self.plane + simulation=self.simulation, plane=self.plane, transpose=transpose ) return self.simulation.plot_grid( - x=a_center[0], y=a_center[1], z=a_center[2], hlim=h_lim, vlim=v_lim, ax=ax, **kwargs + x=a_center[0], + y=a_center[1], + z=a_center[2], + hlim=h_lim, + vlim=v_lim, + ax=ax, + transpose=transpose, + **kwargs, ) @classmethod - def _plane_grid(cls, simulation: Simulation, plane: Box) -> tuple[Coords, Coords]: + def _plane_grid( + cls, + simulation: Simulation, + plane: Box, + transpose: bool = False, + ) -> tuple[Coords, Coords]: """Plane grid for mode solver.""" # Get the mode plane normal axis, center, and limits. - _, _, _, t_axes = cls._center_and_lims(simulation=simulation, plane=plane) + _, _, _, t_axes = cls._center_and_lims( + simulation=simulation, plane=plane, transpose=transpose + ) - grid_snapped = cls._grid_snapped(simulation=simulation, plane=plane) + grid_snapped = cls._grid_snapped(simulation=simulation, plane=plane, transpose=transpose) # Mode plane grid. plane_grid = grid_snapped.boundaries.to_list @@ -2288,10 +2334,14 @@ def _plane_grid(cls, simulation: Simulation, plane: Box) -> tuple[Coords, Coords @classmethod def _effective_num_pml( - cls, simulation: Simulation, plane: Box, mode_spec: ModeSpec + cls, + simulation: Simulation, + plane: Box, + mode_spec: ModeSpec, + transpose: bool = False, ) -> tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]: """Number of cells of the mode solver pml.""" - coord_0, coord_1 = cls._plane_grid(simulation=simulation, plane=plane) + coord_0, coord_1 = cls._plane_grid(simulation=simulation, plane=plane, transpose=transpose) # Number of PML layers in ModeSpec. num_pml_0 = mode_spec.num_pml[0] @@ -2302,7 +2352,11 @@ def _effective_num_pml( @classmethod def _pml_thickness( - cls, simulation: Simulation, plane: Box, mode_spec: ModeSpec + cls, + simulation: Simulation, + plane: Box, + mode_spec: ModeSpec, + transpose: bool = False, ) -> tuple[ tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat], tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat], @@ -2313,12 +2367,14 @@ def _pml_thickness( to the mode plane. """ # Get the mode plane normal axis, center, and limits. - solver_symmetry = cls._solver_symmetry(simulation=simulation, plane=plane) - coord_0, coord_1 = cls._plane_grid(simulation=simulation, plane=plane) + solver_symmetry = cls._solver_symmetry( + simulation=simulation, plane=plane, transpose=transpose + ) + coord_0, coord_1 = cls._plane_grid(simulation=simulation, plane=plane, transpose=transpose) # Number of PML layers in ModeSpec. num_pml_0, num_pml_1 = cls._effective_num_pml( - simulation=simulation, plane=plane, mode_spec=mode_spec + simulation=simulation, plane=plane, mode_spec=mode_spec, transpose=transpose ) # Calculate PML thickness. @@ -2342,20 +2398,31 @@ def _pml_thickness( @classmethod def _mode_plane_size( - cls, simulation: Simulation, plane: Box + cls, simulation: Simulation, plane: Box, transpose: bool = False ) -> tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]: """The size of the mode plane intersected with the simulation.""" - _, h_lim, v_lim, _ = cls._center_and_lims(simulation=simulation, plane=plane) + _, h_lim, v_lim, _ = cls._center_and_lims( + simulation=simulation, plane=plane, transpose=transpose + ) return h_lim[1] - h_lim[0], v_lim[1] - v_lim[0] @classmethod def _mode_plane_size_no_pml( - cls, simulation: Simulation, plane: Box, mode_spec: ModeSpec + cls, + simulation: Simulation, + plane: Box, + mode_spec: ModeSpec, + transpose: bool = False, ) -> tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]: """The size of the remaining portion of the mode plane, after the pml has been removed.""" - size = cls._mode_plane_size(simulation=simulation, plane=plane) - pml_thickness = cls._pml_thickness(simulation=simulation, plane=plane, mode_spec=mode_spec) + size = cls._mode_plane_size(simulation=simulation, plane=plane, transpose=transpose) + pml_thickness = cls._pml_thickness( + simulation=simulation, + plane=plane, + mode_spec=mode_spec, + transpose=transpose, + ) size0 = size[0] - pml_thickness[0][0] - pml_thickness[0][1] size1 = size[1] - pml_thickness[1][0] - pml_thickness[1][1] return size0, size1 @@ -2363,6 +2430,7 @@ def _mode_plane_size_no_pml( def plot_pml( self, ax: Ax = None, + transpose: bool = False, ) -> Ax: """Plot the mode plane absorbing boundaries. @@ -2370,6 +2438,8 @@ def plot_pml( ---------- ax : matplotlib.axes._subplots.Axes = None Matplotlib axes to plot on, if not specified, one is created. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -2377,16 +2447,27 @@ def plot_pml( The supplied or created matplotlib axes. """ return self._plot_pml( - simulation=self.simulation, plane=self.plane, mode_spec=self.mode_spec, ax=ax + simulation=self.simulation, + plane=self.plane, + mode_spec=self.mode_spec, + ax=ax, + transpose=transpose, ) @classmethod def _plot_pml( - cls, simulation: Simulation, plane: Box, mode_spec: ModeSpec, ax: Ax = None + cls, + simulation: Simulation, + plane: Box, + mode_spec: ModeSpec, + ax: Ax = None, + transpose: bool = False, ) -> Ax: """Plot the mode plane absorbing boundaries.""" # Get the mode plane normal axis, center, and limits. - _, h_lim, v_lim, _ = cls._center_and_lims(simulation=simulation, plane=plane) + _, h_lim, v_lim, _ = cls._center_and_lims( + simulation=simulation, plane=plane, transpose=transpose + ) # Create ax if ax=None. if not ax: @@ -2399,11 +2480,13 @@ def _plot_pml( num_pml_1 = mode_spec.num_pml[1] ((pml_thick_0_plus, pml_thick_0_minus), (pml_thick_1_plus, pml_thick_1_minus)) = ( - cls._pml_thickness(simulation=simulation, plane=plane, mode_spec=mode_spec) + cls._pml_thickness( + simulation=simulation, plane=plane, mode_spec=mode_spec, transpose=transpose + ) ) # Mode Plane width and height - mp_w, mp_h = cls._mode_plane_size(simulation=simulation, plane=plane) + mp_w, mp_h = cls._mode_plane_size(simulation=simulation, plane=plane, transpose=transpose) # Plot the absorbing layers. if num_pml_0 > 0 or num_pml_1 > 0: @@ -2434,16 +2517,24 @@ def _plot_pml( return ax @staticmethod - def _center_and_lims(simulation: Simulation, plane: Box) -> tuple[list, list, list, list]: + def _center_and_lims( + simulation: Simulation, + plane: Box, + transpose: bool = False, + ) -> tuple[list, list, list, list]: """Get the mode plane center and limits.""" normal_axis = plane.size.index(0.0) - n_axis, t_axes = plane.pop_axis([0, 1, 2], normal_axis) + n_axis, t_axes = plane.pop_axis_and_swap([0, 1, 2], normal_axis, transpose=transpose) a_center = [None, None, None] a_center[n_axis] = plane.center[n_axis] - _, (h_min_s, v_min_s) = Box.pop_axis(simulation.bounds[0], axis=n_axis) - _, (h_max_s, v_max_s) = Box.pop_axis(simulation.bounds[1], axis=n_axis) + _, (h_min_s, v_min_s) = Box.pop_axis_and_swap( + simulation.bounds[0], axis=n_axis, transpose=transpose + ) + _, (h_max_s, v_max_s) = Box.pop_axis_and_swap( + simulation.bounds[1], axis=n_axis, transpose=transpose + ) h_min = plane.center[t_axes[0]] - plane.size[t_axes[0]] / 2 h_max = plane.center[t_axes[0]] + plane.size[t_axes[0]] / 2 diff --git a/tidy3d/components/monitor.py b/tidy3d/components/monitor.py index 1ad5cef33..80fea120a 100644 --- a/tidy3d/components/monitor.py +++ b/tidy3d/components/monitor.py @@ -358,11 +358,12 @@ def plot( y: Optional[float] = None, z: Optional[float] = None, ax: Ax = None, + transpose: bool = False, **patch_kwargs, ) -> Ax: """Plot this monitor.""" # call the monitor.plot() function first - ax = super().plot(x=x, y=y, z=z, ax=ax, **patch_kwargs) + ax = super().plot(x=x, y=y, z=z, ax=ax, transpose=transpose, **patch_kwargs) kwargs_alpha = patch_kwargs.get("alpha") arrow_alpha = ARROW_ALPHA if kwargs_alpha is None else kwargs_alpha @@ -384,6 +385,7 @@ def plot( color=ARROW_COLOR_MONITOR, alpha=arrow_alpha, both_dirs=True, + transpose=transpose, ) return ax @@ -988,7 +990,10 @@ def local_origin(self) -> Coordinate: return self.center return self.custom_origin - def window_parameters(self, custom_bounds: Bound = None) -> tuple[Size, Coordinate, Coordinate]: + def window_parameters( + self, + custom_bounds: Bound = None, + ) -> tuple[Size, Coordinate, Coordinate]: """Return the physical size of the window transition region based on the monitor's size and optional custom bounds (useful in case the monitor has infinite dimensions). The window size is returned in 3D. Also returns the coordinate where the transition region beings on diff --git a/tidy3d/components/scene.py b/tidy3d/components/scene.py index 17752120a..2e84e881a 100644 --- a/tidy3d/components/scene.py +++ b/tidy3d/components/scene.py @@ -282,7 +282,9 @@ def all_structures(self) -> list[Structure]: @staticmethod def intersecting_media( - test_object: Box, structures: tuple[Structure, ...] + test_object: Box, + structures: tuple[Structure, ...], + transpose: bool = False, ) -> tuple[StructureMediumType, ...]: """From a given list of structures, returns a list of :class:`.AbstractMedium` associated with those structures that intersect with the ``test_object``, if it is a surface, or its @@ -294,6 +296,8 @@ def intersecting_media( Object for which intersecting media are to be detected. structures : List[:class:`.AbstractMedium`] List of structures whose media will be tested. + transpose : bool = False + Optional: Swap the coordinates of test_object in the plane before calculating intersections. Returns ------- @@ -303,7 +307,9 @@ def intersecting_media( structures = [s.to_static() for s in structures] if test_object.size.count(0.0) == 1: # get all merged structures on the test_object, which is already planar - structures_merged = Scene._filter_structures_plane_medium(structures, test_object) + structures_merged = Scene._filter_structures_plane_medium( + structures, test_object, transpose=transpose + ) mediums = {medium for medium, _ in structures_merged} return mediums @@ -317,7 +323,9 @@ def intersecting_media( @staticmethod def intersecting_structures( - test_object: Box, structures: tuple[Structure, ...] + test_object: Box, + structures: tuple[Structure, ...], + transpose: bool = False, ) -> tuple[Structure, ...]: """From a given list of structures, returns a list of :class:`.Structure` that intersect with the ``test_object``, if it is a surface, or its surfaces, if it is a volume. @@ -328,6 +336,8 @@ def intersecting_structures( Object for which intersecting media are to be detected. structures : List[:class:`.AbstractMedium`] List of structures whose media will be tested. + transpose : bool = False + Optional: Swap the coordinates of test_object in the plane before calculating intersections. Returns ------- @@ -344,7 +354,9 @@ def intersecting_structures( structures_merged = [] for structure in structures: - intersections = structure.geometry.intersections_plane(**xyz_kwargs) + intersections = structure.geometry.intersections_plane( + **xyz_kwargs, transpose=transpose + ) if len(intersections) > 0: structures_merged.append(structure) return structures_merged @@ -366,11 +378,12 @@ def _get_plot_lims( z: Optional[float] = None, hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, + transpose: bool = False, ) -> tuple[tuple[float, float], tuple[float, float]]: # if no hlim and/or vlim given, the bounds will then be the usual pml bounds axis, _ = Box.parse_xyz_kwargs(x=x, y=y, z=z) - _, (hmin, vmin) = Box.pop_axis(bounds[0], axis=axis) - _, (hmax, vmax) = Box.pop_axis(bounds[1], axis=axis) + _, (hmin, vmin) = Box.pop_axis_and_swap(bounds[0], axis=axis, transpose=transpose) + _, (hmax, vmax) = Box.pop_axis_and_swap(bounds[1], axis=axis, transpose=transpose) # account for unordered limits if hlim is None: @@ -396,6 +409,7 @@ def plot( hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, fill_structures: bool = True, + transpose: bool = False, **patch_kwargs, ) -> Ax: """Plot each of scene's components on a plane defined by one nonzero x,y,z coordinate. @@ -416,6 +430,8 @@ def plot( The z range if plotting on xz or yz planes, y plane if plotting on xy plane. fill_structures : bool = True Whether to fill structures with color or just draw outlines. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -423,10 +439,16 @@ def plot( The supplied or created matplotlib axes. """ - hlim, vlim = Scene._get_plot_lims(bounds=self.bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + hlim, vlim = Scene._get_plot_lims( + bounds=self.bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose + ) - ax = self.plot_structures(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, fill=fill_structures) - ax = self._set_plot_bounds(bounds=self.bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + ax = self.plot_structures( + ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, fill=fill_structures, transpose=transpose + ) + ax = self._set_plot_bounds( + bounds=self.bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose + ) return ax @equal_aspect @@ -440,6 +462,7 @@ def plot_structures( hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, fill: bool = True, + transpose: bool = False, ) -> Ax: """Plot each of scene's structures on a plane defined by one nonzero x,y,z coordinate. @@ -459,15 +482,22 @@ def plot_structures( The z range if plotting on xz or yz planes, y plane if plotting on xy plane. fill : bool = True Whether to fill structures with color or just draw outlines. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ - medium_shapes = self._get_structures_2dbox( - structures=self.to_static().sorted_structures, x=x, y=y, z=z, hlim=hlim, vlim=vlim + structures=self.to_static().sorted_structures, + x=x, + y=y, + z=z, + hlim=hlim, + vlim=vlim, + transpose=transpose, ) medium_map = self.medium_map for medium, shape in medium_shapes: @@ -483,10 +513,12 @@ def plot_structures( # clean up the axis display axis, _ = Box.parse_xyz_kwargs(x=x, y=y, z=z) ax = self.box.add_ax_lims(axis=axis, ax=ax) - ax = self._set_plot_bounds(bounds=self.bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + ax = self._set_plot_bounds( + bounds=self.bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose + ) # Add the default axis labels, tick labels, and title ax = Box.add_ax_labels_and_title( - ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units + ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units, transpose=transpose ) return ax @@ -580,6 +612,7 @@ def _set_plot_bounds( z: Optional[float] = None, hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, + transpose: bool = False, ) -> Ax: """Sets the xy limits of the scene at a plane, useful after plotting. @@ -597,13 +630,17 @@ def _set_plot_bounds( The x range if plotting on xy or xz planes, y range if plotting on yz plane. vlim : Tuple[float, float] = None The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) + Returns ------- matplotlib.axes._subplots.Axes The axes after setting the boundaries. """ - - hlim, vlim = Scene._get_plot_lims(bounds=bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + hlim, vlim = Scene._get_plot_lims( + bounds=bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose + ) ax.set_xlim(hlim) ax.set_ylim(vlim) return ax @@ -616,6 +653,7 @@ def _get_structures_2dbox( z: Optional[float] = None, hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, + transpose: bool = False, ) -> list[tuple[Medium, Shapely]]: """Compute list of shapes to plot on 2d box specified by (x_min, x_max), (y_min, y_max). @@ -633,6 +671,8 @@ def _get_structures_2dbox( The x range if plotting on xy or xz planes, y range if plotting on yz plane. vlim : Tuple[float, float] = None The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -641,8 +681,8 @@ def _get_structures_2dbox( """ # if no hlim and/or vlim given, the bounds will then be the usual pml bounds axis, _ = Box.parse_xyz_kwargs(x=x, y=y, z=z) - _, (hmin, vmin) = Box.pop_axis(self.bounds[0], axis=axis) - _, (hmax, vmax) = Box.pop_axis(self.bounds[1], axis=axis) + _, (hmin, vmin) = Box.pop_axis_and_swap(self.bounds[0], axis=axis, transpose=transpose) + _, (hmax, vmax) = Box.pop_axis_and_swap(self.bounds[1], axis=axis, transpose=transpose) if hlim is not None: (hmin, hmax) = hlim @@ -656,13 +696,15 @@ def _get_structures_2dbox( v_size = (vmax - vmin) or inf axis, center_normal = Box.parse_xyz_kwargs(x=x, y=y, z=z) - center = Box.unpop_axis(center_normal, (h_center, v_center), axis=axis) - size = Box.unpop_axis(0.0, (h_size, v_size), axis=axis) + center = Box.unpop_axis_and_swap( + center_normal, (h_center, v_center), axis=axis, transpose=transpose + ) + size = Box.unpop_axis_and_swap(0.0, (h_size, v_size), axis=axis, transpose=transpose) plane = Box(center=center, size=size) medium_shapes = [] for structure in structures: - intersections = plane.intersections_with(structure.geometry) + intersections = plane.intersections_with(structure.geometry, transpose=transpose) for shape in intersections: if not shape.is_empty: shape = Box.evaluate_inf_shape(shape) @@ -671,7 +713,9 @@ def _get_structures_2dbox( @staticmethod def _filter_structures_plane_medium( - structures: list[Structure], plane: Box + structures: list[Structure], + plane: Box, + transpose: bool = False, ) -> list[tuple[Medium, Shapely]]: """Compute list of shapes to plot on plane. Overlaps are removed or merged depending on medium. @@ -682,6 +726,8 @@ def _filter_structures_plane_medium( List of structures to filter on the plane. plane : Box Plane specification. + transpose : bool = False + Optional: Swap the coordinates in the plane before calculating intersections. Returns ------- @@ -691,7 +737,7 @@ def _filter_structures_plane_medium( medium_list = [structure.medium for structure in structures] return Scene._filter_structures_plane( - structures=structures, plane=plane, property_list=medium_list + structures=structures, plane=plane, property_list=medium_list, transpose=transpose ) @staticmethod @@ -699,6 +745,7 @@ def _filter_structures_plane( structures: list[Structure], plane: Box, property_list: list, + transpose: bool = False, ) -> list[tuple[Medium, Shapely]]: """Compute list of shapes to plot on plane. Overlaps are removed or merged depending on provided property_list. @@ -711,6 +758,8 @@ def _filter_structures_plane( Plane specification. property_list : List = None Property value for each structure. + transpose : bool = False + Optional: Swap the coordinates in the plane before calculating intersections. Returns ------- @@ -718,7 +767,10 @@ def _filter_structures_plane( List of shapes and their property value on the plane after merging. """ return merging_geometries_on_plane( - [structure.geometry for structure in structures], plane, property_list + [structure.geometry for structure in structures], + plane, + property_list, + transpose=transpose, ) """ Plotting Optical """ @@ -735,6 +787,7 @@ def plot_eps( ax: Ax = None, hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, + transpose: bool = False, ) -> Ax: """Plot each of scene's components on a plane defined by one nonzero x,y,z coordinate. The permittivity is plotted in grayscale based on its value at the specified frequency. @@ -759,6 +812,8 @@ def plot_eps( The x range if plotting on xy or xz planes, y range if plotting on yz plane. vlim : Tuple[float, float] = None The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -766,12 +821,25 @@ def plot_eps( The supplied or created matplotlib axes. """ - hlim, vlim = Scene._get_plot_lims(bounds=self.bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + hlim, vlim = Scene._get_plot_lims( + bounds=self.bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose + ) ax = self.plot_structures_eps( - freq=freq, cbar=True, alpha=alpha, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + freq=freq, + cbar=True, + alpha=alpha, + ax=ax, + x=x, + y=y, + z=z, + hlim=hlim, + vlim=vlim, + transpose=transpose, + ) + ax = self._set_plot_bounds( + bounds=self.bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose ) - ax = self._set_plot_bounds(bounds=self.bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) return ax @equal_aspect @@ -791,6 +859,7 @@ def plot_structures_eps( vlim: Optional[tuple[float, float]] = None, grid: Grid = None, eps_component: Optional[PermittivityComponent] = None, + transpose: bool = False, ) -> Ax: """Plot each of scene's structures on a plane defined by one nonzero x,y,z coordinate. The permittivity is plotted in grayscale based on its value at the specified frequency. @@ -826,6 +895,8 @@ def plot_structures_eps( Component of the permittivity tensor to plot for anisotropic materials, e.g. ``"xx"``, ``"yy"``, ``"zz"``, ``"xy"``, ``"yz"``, ... Defaults to ``None``, which returns the average of the diagonal values. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -848,6 +919,7 @@ def plot_structures_eps( grid=grid, property="eps", eps_component=eps_component, + transpose=transpose, ) @equal_aspect @@ -868,6 +940,7 @@ def plot_structures_property( grid: Grid = None, property: Literal["eps", "doping", "N_a", "N_d"] = "eps", eps_component: Optional[PermittivityComponent] = None, + transpose: bool = False, ) -> Ax: """Plot each of scene's structures on a plane defined by one nonzero x,y,z coordinate. The permittivity is plotted in grayscale based on its value at the specified frequency. @@ -906,6 +979,8 @@ def plot_structures_property( Component of the permittivity tensor to plot for anisotropic materials, e.g. ``"xx"``, ``"yy"``, ``"zz"``, ``"xy"``, ``"yz"``, ... Defaults to ``None``, which returns the average of the diagonal values. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -937,11 +1012,13 @@ def plot_structures_property( # that needs to be rendered if property in ["N_d", "N_a", "doping"]: structures = [self.background_structure, *list(structures)] - medium_shapes = self._filter_structures_plane_medium(structures=structures, plane=plane) + medium_shapes = self._filter_structures_plane_medium( + structures=structures, plane=plane, transpose=transpose + ) else: structures = [self.background_structure, *list(structures)] medium_shapes = self._get_structures_2dbox( - structures=structures, x=x, y=y, z=z, hlim=hlim, vlim=vlim + structures=structures, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose ) property_min, property_max = limits @@ -1036,10 +1113,12 @@ def plot_structures_property( # clean up the axis display axis, _ = Box.parse_xyz_kwargs(x=x, y=y, z=z) ax = self.box.add_ax_lims(axis=axis, ax=ax) - ax = self._set_plot_bounds(bounds=self.bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + ax = self._set_plot_bounds( + bounds=self.bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose + ) # Add the default axis labels, tick labels, and title ax = Box.add_ax_labels_and_title( - ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units + ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units, transpose=transpose ) return ax @@ -1339,8 +1418,9 @@ def plot_heat_charge_property( ax: Ax = None, hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, + transpose: bool = False, ) -> Ax: - """Plot each of scebe's components on a plane defined by one nonzero x,y,z coordinate. + """Plot each of scene's components on a plane defined by one nonzero x,y,z coordinate. The thermal conductivity is plotted in grayscale based on its value. Parameters @@ -1365,6 +1445,8 @@ def plot_heat_charge_property( The x range if plotting on xy or xz planes, y range if plotting on yz plane. vlim : Tuple[float, float] = None The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -1372,12 +1454,25 @@ def plot_heat_charge_property( The supplied or created matplotlib axes. """ - hlim, vlim = Scene._get_plot_lims(bounds=self.bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + hlim, vlim = Scene._get_plot_lims( + bounds=self.bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose + ) ax = self.plot_structures_heat_charge_property( - cbar=cbar, alpha=alpha, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, property=property + cbar=cbar, + alpha=alpha, + ax=ax, + x=x, + y=y, + z=z, + hlim=hlim, + vlim=vlim, + property=property, + transpose=transpose, + ) + ax = self._set_plot_bounds( + bounds=self.bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose ) - ax = self._set_plot_bounds(bounds=self.bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) return ax @equal_aspect @@ -1393,6 +1488,7 @@ def plot_structures_heat_conductivity( ax: Ax = None, hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, + transpose: bool = False, ) -> Ax: """Plot each of scene's structures on a plane defined by one nonzero x,y,z coordinate. The thermal conductivity is plotted in grayscale based on its value. @@ -1419,6 +1515,8 @@ def plot_structures_heat_conductivity( The x range if plotting on xy or xz planes, y range if plotting on yz plane. vlim : Tuple[float, float] = None The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -1443,6 +1541,7 @@ def plot_structures_heat_conductivity( ax=ax, hlim=hlim, vlim=vlim, + transpose=transpose, ) @equal_aspect @@ -1459,6 +1558,7 @@ def plot_structures_heat_charge_property( ax: Ax = None, hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, + transpose: bool = False, ) -> Ax: """Plot each of scene's structures on a plane defined by one nonzero x,y,z coordinate. The thermal conductivity is plotted in grayscale based on its value. @@ -1485,6 +1585,8 @@ def plot_structures_heat_charge_property( The x range if plotting on xy or xz planes, y range if plotting on yz plane. vlim : Tuple[float, float] = None The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -1506,11 +1608,13 @@ def plot_structures_heat_charge_property( center = Box.unpop_axis(position, (0, 0), axis=axis) size = Box.unpop_axis(0, (inf, inf), axis=axis) plane = Box(center=center, size=size) - medium_shapes = self._filter_structures_plane_medium(structures=structures, plane=plane) + medium_shapes = self._filter_structures_plane_medium( + structures=structures, plane=plane, transpose=transpose + ) else: structures = [self.background_structure, *list(structures)] medium_shapes = self._get_structures_2dbox( - structures=structures, x=x, y=y, z=z, hlim=hlim, vlim=vlim + structures=structures, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose ) property_val_min, property_val_max = self.heat_charge_property_bounds(property=property) @@ -1543,10 +1647,12 @@ def plot_structures_heat_charge_property( # clean up the axis display axis, _ = Box.parse_xyz_kwargs(x=x, y=y, z=z) ax = self.box.add_ax_lims(axis=axis, ax=ax) - ax = self._set_plot_bounds(bounds=self.bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + ax = self._set_plot_bounds( + bounds=self.bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose + ) # Add the default axis labels, tick labels, and title ax = Box.add_ax_labels_and_title( - ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units + ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units, transpose=transpose ) return ax @@ -1676,6 +1782,7 @@ def plot_heat_conductivity( ax: Ax = None, hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, + transpose: bool = False, ): """Plot each of scebe's components on a plane defined by one nonzero x,y,z coordinate. The thermal conductivity is plotted in grayscale based on its value. @@ -1699,6 +1806,8 @@ def plot_heat_conductivity( The x range if plotting on xy or xz planes, y range if plotting on yz plane. vlim : Tuple[float, float] = None The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -1722,6 +1831,7 @@ def plot_heat_conductivity( ax=ax, hlim=hlim, vlim=vlim, + transpose=transpose, ) """ Misc """ diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index 852159f30..3537c59ea 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -455,6 +455,7 @@ def plot( hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, fill_structures: bool = True, + transpose: bool = False, **patch_kwargs, ) -> Ax: """Plot each of simulation's components on a plane defined by one nonzero x,y,z coordinate. @@ -481,6 +482,8 @@ def plot( The x range if plotting on xy or xz planes, y range if plotting on yz plane. vlim : Tuple[float, float] = None The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -495,9 +498,8 @@ def plot( """ hlim, vlim = Scene._get_plot_lims( - bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim + bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose ) - ax = self.scene.plot( x=x, y=y, @@ -506,20 +508,37 @@ def plot( hlim=hlim, vlim=vlim, fill_structures=fill_structures, + transpose=transpose, + ) + ax = self.plot_sources( + ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=source_alpha, transpose=transpose + ) + ax = self.plot_monitors( + ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=monitor_alpha, transpose=transpose ) - - ax = self.plot_sources(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=source_alpha) - ax = self.plot_monitors(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=monitor_alpha) ax = self.plot_lumped_elements( - ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=lumped_element_alpha + ax=ax, + x=x, + y=y, + z=z, + hlim=hlim, + vlim=vlim, + alpha=lumped_element_alpha, + transpose=transpose, ) - ax = self.plot_symmetries(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) - ax = self.plot_pml(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + ax = self.plot_symmetries(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose) + ax = self.plot_pml(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose) ax = Scene._set_plot_bounds( - bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + bounds=self.simulation_bounds, + ax=ax, + x=x, + y=y, + z=z, + hlim=hlim, + vlim=vlim, + transpose=transpose, ) - ax = self.plot_boundaries(ax=ax, x=x, y=y, z=z) - + ax = self.plot_boundaries(ax=ax, x=x, y=y, z=z, transpose=transpose) return ax @equal_aspect @@ -539,6 +558,7 @@ def plot_eps( ax: Ax = None, eps_component: Optional[PermittivityComponent] = None, eps_lim: tuple[Union[float, None], Union[float, None]] = (None, None), + transpose: bool = False, ) -> Ax: """Plot each of simulation's components on a plane defined by one nonzero x,y,z coordinate. The permittivity is plotted in grayscale based on its value at the specified frequency. @@ -577,6 +597,8 @@ def plot_eps( Defaults to ``None``, which returns the average of the diagonal values. eps_lim : Tuple[float, float] = None Custom limits for eps coloring. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -600,7 +622,7 @@ def plot_eps( ) hlim, vlim = Scene._get_plot_lims( - bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim + bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose ) ax = self.plot_structures_eps( @@ -615,18 +637,37 @@ def plot_eps( vlim=vlim, eps_component=eps_component, eps_lim=eps_lim, + transpose=transpose, + ) + ax = self.plot_sources( + ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=source_alpha, transpose=transpose + ) + ax = self.plot_monitors( + ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=monitor_alpha, transpose=transpose ) - ax = self.plot_sources(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=source_alpha) - ax = self.plot_monitors(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=monitor_alpha) ax = self.plot_lumped_elements( - ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=lumped_element_alpha + ax=ax, + x=x, + y=y, + z=z, + hlim=hlim, + vlim=vlim, + alpha=lumped_element_alpha, + transpose=transpose, ) - ax = self.plot_symmetries(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) - ax = self.plot_pml(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + ax = self.plot_symmetries(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose) + ax = self.plot_pml(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose) ax = Scene._set_plot_bounds( - bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + bounds=self.simulation_bounds, + ax=ax, + x=x, + y=y, + z=z, + hlim=hlim, + vlim=vlim, + transpose=transpose, ) - ax = self.plot_boundaries(ax=ax, x=x, y=y, z=z) + ax = self.plot_boundaries(ax=ax, x=x, y=y, z=z, transpose=transpose) return ax @equal_aspect @@ -645,6 +686,7 @@ def plot_structures_eps( vlim: Optional[tuple[float, float]] = None, eps_component: Optional[PermittivityComponent] = None, eps_lim: tuple[Union[float, None], Union[float, None]] = (None, None), + transpose: bool = False, ) -> Ax: """Plot each of simulation's structures on a plane defined by one nonzero x,y,z coordinate. The permittivity is plotted in grayscale based on its value at the specified frequency. @@ -682,6 +724,8 @@ def plot_structures_eps( Defaults to ``None``, which returns the average of the diagonal values. eps_lim : Tuple[float, float] = None Custom limits for eps coloring. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -690,7 +734,7 @@ def plot_structures_eps( """ hlim, vlim = Scene._get_plot_lims( - bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim + bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose ) if freq is None: freq0s = [source.source_time.freq0 for source in self.sources] @@ -718,6 +762,7 @@ def plot_structures_eps( reverse=reverse, eps_component=eps_component, eps_lim=eps_lim, + transpose=transpose, ) @equal_aspect @@ -730,6 +775,7 @@ def plot_pml( hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, ax: Ax = None, + transpose: bool = False, ) -> Ax: """Plot each of simulation's absorbing boundaries on a plane defined by one nonzero x,y,z coordinate. @@ -748,6 +794,8 @@ def plot_pml( The z range if plotting on xz or yz planes, y plane if plotting on xy plane. ax : matplotlib.axes._subplots.Axes = None Matplotlib axes to plot on, if not specified, one is created. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -757,13 +805,20 @@ def plot_pml( normal_axis, _ = self.parse_xyz_kwargs(x=x, y=y, z=z) pml_boxes = self._make_pml_boxes(normal_axis=normal_axis) for pml_box in pml_boxes: - pml_box.plot(x=x, y=y, z=z, ax=ax, **plot_params_pml.to_kwargs()) + pml_box.plot(x=x, y=y, z=z, ax=ax, transpose=transpose, **plot_params_pml.to_kwargs()) ax = Scene._set_plot_bounds( - bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + bounds=self.simulation_bounds, + ax=ax, + x=x, + y=y, + z=z, + hlim=hlim, + vlim=vlim, + transpose=transpose, ) # Add the default axis labels, tick labels, and title ax = Box.add_ax_labels_and_title( - ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units + ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units, transpose=transpose ) return ax @@ -888,6 +943,7 @@ def plot_lumped_elements( vlim: Optional[tuple[float, float]] = None, alpha: Optional[float] = None, ax: Ax = None, + transpose: bool = False, ) -> Ax: """Plot each of simulation's lumped elements on a plane defined by one nonzero x,y,z coordinate. @@ -908,6 +964,8 @@ def plot_lumped_elements( Opacity of the lumped element, If ``None`` uses Tidy3d default. ax : matplotlib.axes._subplots.Axes = None Matplotlib axes to plot on, if not specified, one is created. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -917,9 +975,18 @@ def plot_lumped_elements( bounds = self.bounds for element in self.lumped_elements: kwargs = element.plot_params.include_kwargs(alpha=alpha).to_kwargs() - ax = element.to_geometry().plot(x=x, y=y, z=z, ax=ax, sim_bounds=bounds, **kwargs) + ax = element.to_geometry().plot( + x=x, y=y, z=z, ax=ax, sim_bounds=bounds, transpose=transpose, **kwargs + ) ax = Scene._set_plot_bounds( - bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + bounds=self.simulation_bounds, + ax=ax, + x=x, + y=y, + z=z, + hlim=hlim, + vlim=vlim, + transpose=transpose, ) return ax @@ -934,6 +1001,7 @@ def plot_grid( vlim: Optional[tuple[float, float]] = None, override_structures_alpha: float = 1, snapping_points_alpha: float = 1, + transpose: bool = False, **kwargs, ) -> Ax: """Plot the cell boundaries as lines on a plane defined by one nonzero x,y,z coordinate. @@ -956,6 +1024,8 @@ def plot_grid( Opacity of the snapping points. ax : matplotlib.axes._subplots.Axes = None Matplotlib axes to plot on, if not specified, one is created. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) **kwargs Optional keyword arguments passed to the matplotlib ``LineCollection``. For details on accepted values, refer to @@ -974,7 +1044,7 @@ def plot_grid( kwargs.setdefault("snapping_linestyle", "--") cell_boundaries = self.grid.boundaries axis, _ = self.parse_xyz_kwargs(x=x, y=y, z=z) - _, (axis_x, axis_y) = self.pop_axis([0, 1, 2], axis=axis) + _, (axis_x, axis_y) = self.pop_axis_and_swap([0, 1, 2], axis=axis, transpose=transpose) boundaries_x = cell_boundaries.dict()["xyz"[axis_x]] boundaries_y = cell_boundaries.dict()["xyz"[axis_y]] @@ -1010,7 +1080,9 @@ def plot_grid( for structures, plot_param in zip(all_override_structures, plot_params): for structure in structures: bounds = list(zip(*structure.geometry.bounds)) - _, ((xmin, xmax), (ymin, ymax)) = structure.geometry.pop_axis(bounds, axis=axis) + _, ((xmin, xmax), (ymin, ymax)) = structure.geometry.pop_axis_and_swap( + bounds, axis=axis, transpose=transpose + ) xmin, xmax, ymin, ymax = ( self._evaluate_inf(v) for v in (xmin, xmax, ymin, ymax) ) @@ -1033,7 +1105,9 @@ def plot_grid( plot_params, ): for point in points: - _, (x_point, y_point) = Geometry.pop_axis(point, axis=axis) + _, (x_point, y_point) = Geometry.pop_axis_and_swap( + point, axis=axis, transpose=transpose + ) if x_point is None and y_point is None: continue if x_point is None: @@ -1062,11 +1136,18 @@ def plot_grid( ) ax = Scene._set_plot_bounds( - bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + bounds=self.simulation_bounds, + ax=ax, + x=x, + y=y, + z=z, + hlim=hlim, + vlim=vlim, + transpose=transpose, ) # Add the default axis labels, tick labels, and title ax = Box.add_ax_labels_and_title( - ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units + ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units, transpose=transpose ) return ax @@ -1078,6 +1159,7 @@ def plot_boundaries( y: Optional[float] = None, z: Optional[float] = None, ax: Ax = None, + transpose: bool = False, **kwargs, ) -> Ax: """Plot the simulation boundary conditions as lines on a plane @@ -1093,6 +1175,8 @@ def plot_boundaries( position of plane in z direction, only one of x, y, z must be specified to define plane. ax : matplotlib.axes._subplots.Axes = None Matplotlib axes to plot on, if not specified, one is created. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) **kwargs Optional keyword arguments passed to the matplotlib ``LineCollection``. For details on accepted values, refer to @@ -1128,7 +1212,7 @@ def set_plot_params(boundary_edge, lim, side, thickness): boundaries = self.boundary_spec.to_list normal_axis, _ = self.parse_xyz_kwargs(x=x, y=y, z=z) - _, (dim_u, dim_v) = self.pop_axis([0, 1, 2], axis=normal_axis) + _, (dim_u, dim_v) = self.pop_axis_and_swap([0, 1, 2], axis=normal_axis, transpose=transpose) umin, umax = ax.get_xlim() vmin, vmax = ax.get_ylim() @@ -1181,12 +1265,12 @@ def set_plot_params(boundary_edge, lim, side, thickness): ) ax.add_patch(rect) - # ax = self._set_plot_bounds(ax=ax, x=x, y=y, z=z) + # ax = self._set_plot_bounds(ax=ax, x=x, y=y, z=z, transpose=transpose) ax.set_xlim([ulim_minus, ulim_plus]) ax.set_ylim([vlim_minus, vlim_plus]) # Add the default axis labels, tick labels, and title ax = Box.add_ax_labels_and_title( - ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units + ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units, transpose=transpose ) return ax diff --git a/tidy3d/components/source/base.py b/tidy3d/components/source/base.py index 388896fb5..d81b06bde 100644 --- a/tidy3d/components/source/base.py +++ b/tidy3d/components/source/base.py @@ -74,6 +74,7 @@ def plot( y: Optional[float] = None, z: Optional[float] = None, ax: Ax = None, + transpose: bool = False, **patch_kwargs, ) -> Ax: """Plot this source.""" @@ -81,7 +82,7 @@ def plot( kwargs_arrow_base = patch_kwargs.pop("arrow_base", None) # call the `Source.plot()` function first. - ax = Box.plot(self, x=x, y=y, z=z, ax=ax, **patch_kwargs) + ax = Box.plot(self, x=x, y=y, z=z, ax=ax, transpose=transpose, **patch_kwargs) kwargs_alpha = patch_kwargs.get("alpha") arrow_alpha = ARROW_ALPHA if kwargs_alpha is None else kwargs_alpha @@ -112,6 +113,7 @@ def plot( alpha=arrow_alpha, both_dirs=False, arrow_base=kwargs_arrow_base, + transpose=transpose, ) if self._pol_vector is not None: @@ -125,6 +127,7 @@ def plot( alpha=arrow_alpha, both_dirs=False, arrow_base=kwargs_arrow_base, + transpose=transpose, ) return ax diff --git a/tidy3d/components/source/field.py b/tidy3d/components/source/field.py index f147b8ec7..eadec736d 100644 --- a/tidy3d/components/source/field.py +++ b/tidy3d/components/source/field.py @@ -704,9 +704,10 @@ def plot( y: Optional[float] = None, z: Optional[float] = None, ax: Ax = None, + transpose: bool = False, **patch_kwargs, ) -> Ax: # call Source.plot but with the base of the arrow centered on the injection plane patch_kwargs["arrow_base"] = self.injection_plane_center - ax = Source.plot(self, x=x, y=y, z=z, ax=ax, **patch_kwargs) + ax = Source.plot(self, x=x, y=y, z=z, ax=ax, transpose=transpose, **patch_kwargs) return ax diff --git a/tidy3d/components/structure.py b/tidy3d/components/structure.py index 17f175a8b..a853bcbcc 100644 --- a/tidy3d/components/structure.py +++ b/tidy3d/components/structure.py @@ -149,6 +149,7 @@ def plot( y: Optional[float] = None, z: Optional[float] = None, ax: Ax = None, + transpose: bool = False, **patch_kwargs, ) -> Ax: """Plot structure's geometric cross section at single (x,y,z) coordinate. @@ -163,6 +164,8 @@ def plot( Position of plane in z direction, only one of x,y,z can be specified to define plane. ax : matplotlib.axes._subplots.Axes = None Matplotlib axes to plot on, if not specified, one is created. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) **patch_kwargs Optional keyword arguments passed to the matplotlib patch plotting of structure. For details on accepted values, refer to @@ -173,7 +176,9 @@ def plot( matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ - return self.geometry.plot(x=x, y=y, z=z, ax=ax, viz_spec=self.viz_spec, **patch_kwargs) + return self.geometry.plot( + x=x, y=y, z=z, ax=ax, viz_spec=self.viz_spec, transpose=transpose, **patch_kwargs + ) class Structure(AbstractStructure): diff --git a/tidy3d/components/tcad/bandgap.py b/tidy3d/components/tcad/bandgap.py index 55c2cedaf..e76557ea9 100644 --- a/tidy3d/components/tcad/bandgap.py +++ b/tidy3d/components/tcad/bandgap.py @@ -35,7 +35,8 @@ class SlotboomBandGapNarrowing(Tidy3dBaseModel): ... min_N=1e15, ... ) - .. [1] 'UNIFIED APPARENT BANDGAP NARROWING IN n- AND p-TYPE SILICON' Solid-State Electronics Vol. 35, No. 2, pp. 125-129, 1992""" + .. [1] 'UNIFIED APPARENT BANDGAP NARROWING IN n- AND p-TYPE SILICON' Solid-State Electronics Vol. 35, No. 2, pp. 125-129, 1992 + """ v1: pd.PositiveFloat = pd.Field( ..., diff --git a/tidy3d/components/tcad/data/sim_data.py b/tidy3d/components/tcad/data/sim_data.py index c33a0d6e6..b4d8d5cc4 100644 --- a/tidy3d/components/tcad/data/sim_data.py +++ b/tidy3d/components/tcad/data/sim_data.py @@ -18,6 +18,7 @@ TriangularGridDataset, UnstructuredGridDataset, ) +from tidy3d.components.geometry.base import Geometry from tidy3d.components.tcad.data.types import ( SteadyPotentialData, TCADMonitorDataType, @@ -157,6 +158,7 @@ def plot_field( vmin: Optional[float] = None, vmax: Optional[float] = None, ax: Ax = None, + transpose: bool = False, **sel_kwargs, ) -> Ax: """Plot the data for a monitor with simulation plot overlaid. @@ -260,10 +262,12 @@ def plot_field( # compute plot bounds field_data_bounds = field_data.bounds - min_bounds = list(field_data_bounds[0]) - max_bounds = list(field_data_bounds[1]) - min_bounds.pop(axis) - max_bounds.pop(axis) + _, min_bounds = Geometry.pop_axis_and_swap( + field_data_bounds[0], axis, transpose=transpose + ) + _, max_bounds = Geometry.pop_axis_and_swap( + field_data_bounds[1], axis, transpose=transpose + ) if isinstance(field_data, SpatialDataArray): # interp out any monitor.size==0 dimensions @@ -315,8 +319,10 @@ def plot_field( axis = "xyz".index(planar_coord) position = float(field_data.coords[planar_coord]) - xy_coord_labels = list("xyz") - xy_coord_labels.pop(axis) + _, xy_coord_labels = Geometry.pop_axis_and_swap( + list("xyz"), axis=axis, transpose=transpose + ) + x_coord_label, y_coord_label = xy_coord_labels[0], xy_coord_labels[1] field_data.plot( ax=ax, @@ -344,6 +350,7 @@ def plot_field( alpha=structures_alpha, ax=ax, property=property_to_plot, + transpose=transpose, **interp_kwarg, ) diff --git a/tidy3d/components/tcad/simulation/heat.py b/tidy3d/components/tcad/simulation/heat.py index ace1d8a85..0a12ee329 100644 --- a/tidy3d/components/tcad/simulation/heat.py +++ b/tidy3d/components/tcad/simulation/heat.py @@ -70,6 +70,7 @@ def plot_heat_conductivity( colorbar: str = "conductivity", hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, + transpose: bool = False, ) -> Ax: """Plot each of simulation's components on a plane defined by one nonzero x,y,z coordinate. @@ -97,6 +98,8 @@ def plot_heat_conductivity( The x range if plotting on xy or xz planes, y range if plotting on yz plane. vlim : Tuple[float, float] = None The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -121,4 +124,5 @@ def plot_heat_conductivity( property=plot_type, hlim=hlim, vlim=vlim, + transpose=transpose, ) diff --git a/tidy3d/components/tcad/simulation/heat_charge.py b/tidy3d/components/tcad/simulation/heat_charge.py index d93ec0b14..a14d56fb4 100644 --- a/tidy3d/components/tcad/simulation/heat_charge.py +++ b/tidy3d/components/tcad/simulation/heat_charge.py @@ -965,6 +965,7 @@ def plot_property( property: str = "heat_conductivity", hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, + transpose: bool = False, ) -> Ax: """Plot each of simulation's components on a plane defined by one nonzero x,y,z coordinate. @@ -992,6 +993,8 @@ def plot_property( The x range if plotting on xy or xz planes, y range if plotting on yz plane. vlim : Tuple[float, float] = None The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -1000,7 +1003,7 @@ def plot_property( """ hlim, vlim = Scene._get_plot_lims( - bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim + bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose ) cbar_cond = True @@ -1035,16 +1038,34 @@ def plot_property( hlim=hlim, vlim=vlim, property=property, + transpose=transpose, ) ax = self.plot_sources( - ax=ax, x=x, y=y, z=z, property=property, alpha=source_alpha, hlim=hlim, vlim=vlim + ax=ax, + x=x, + y=y, + z=z, + property=property, + alpha=source_alpha, + hlim=hlim, + vlim=vlim, + transpose=transpose, + ) + ax = self.plot_monitors( + ax=ax, x=x, y=y, z=z, alpha=monitor_alpha, hlim=hlim, vlim=vlim, transpose=transpose ) - ax = self.plot_monitors(ax=ax, x=x, y=y, z=z, alpha=monitor_alpha, hlim=hlim, vlim=vlim) - ax = self.plot_boundaries(ax=ax, x=x, y=y, z=z, property=property) + ax = self.plot_boundaries(ax=ax, x=x, y=y, z=z, property=property, transpose=transpose) ax = Scene._set_plot_bounds( - bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + bounds=self.simulation_bounds, + ax=ax, + x=x, + y=y, + z=z, + hlim=hlim, + vlim=vlim, + transpose=transpose, ) - ax = self.plot_symmetries(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + ax = self.plot_symmetries(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, transpose=transpose) if property == "source": self._add_source_cbar(ax=ax, property=property) @@ -1064,6 +1085,7 @@ def plot_heat_conductivity( colorbar: str = "conductivity", hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, + transpose: bool = False, **kwargs, ) -> Ax: """ @@ -1094,6 +1116,8 @@ def plot_heat_conductivity( The x range if plotting on xy or xz planes, y range if plotting on yz plane. vlim : Tuple[float, float] = None The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -1123,6 +1147,7 @@ def plot_heat_conductivity( property=plot_type, hlim=hlim, vlim=vlim, + transpose=transpose, ) @equal_aspect @@ -1134,6 +1159,7 @@ def plot_boundaries( z: Optional[float] = None, property: str = "heat_conductivity", ax: Ax = None, + transpose: bool = False, ) -> Ax: """Plot each of simulation's boundary conditions on a plane defined by one nonzero x,y,z coordinate. @@ -1151,6 +1177,8 @@ def plot_boundaries( Options are ["heat_conductivity", "electric_conductivity"] ax : matplotlib.axes._subplots.Axes = None Matplotlib axes to plot on, if not specified, one is created. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -1173,6 +1201,7 @@ def plot_boundaries( structures=structures, plane=plane, boundary_spec=self.boundary_spec, + transpose=transpose, ) # plot boundary conditions @@ -1187,11 +1216,13 @@ def plot_boundaries( ax = self._plot_boundary_condition(shape=shape, boundary_spec=bc_spec, ax=ax) # clean up the axis display - ax = self.add_ax_lims(axis=axis, ax=ax) - ax = Scene._set_plot_bounds(bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z) + ax = self.add_ax_lims(axis=axis, ax=ax, transpose=transpose) + ax = Scene._set_plot_bounds( + bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, transpose=transpose + ) # Add the default axis labels, tick labels, and title ax = Box.add_ax_labels_and_title( - ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units + ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units, transpose=transpose ) return ax @@ -1448,6 +1479,7 @@ def _construct_heat_charge_boundaries( structures: list[Structure], plane: Box, boundary_spec: list[HeatChargeBoundarySpec], + transpose: bool = False, ) -> list[tuple[HeatChargeBoundarySpec, Shapely]]: """Compute list of boundary lines to plot on plane. @@ -1459,6 +1491,8 @@ def _construct_heat_charge_boundaries( target plane. boundary_spec : List[HeatBoundarySpec] list of boundary conditions associated with structures. + transpose : bool = False + Swap the boundary-box coordinates in the plane. (This overrides the default lexicographic axis order.) Returns ------- @@ -1470,7 +1504,7 @@ def _construct_heat_charge_boundaries( shapes = [] # structure name, structure medium, shape, bounds for structure in structures: # get list of Shapely shapes that intersect at the plane - shapes_plane = plane.intersections_with(structure.geometry) + shapes_plane = plane.intersections_with(structure.geometry, transpose=transpose) # append each of them and their medium information to the list of shapes for shape in shapes_plane: @@ -1520,6 +1554,7 @@ def plot_sources( vlim: Optional[tuple[float, float]] = None, alpha: Optional[float] = None, ax: Ax = None, + transpose: bool = False, ) -> Ax: """Plot each of simulation's sources on a plane defined by one nonzero x,y,z coordinate. @@ -1542,6 +1577,8 @@ def plot_sources( Opacity of the sources, If ``None`` uses Tidy3d default. ax : matplotlib.axes._subplots.Axes = None Matplotlib axes to plot on, if not specified, one is created. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default lexicographic axis order.) Returns ------- @@ -1580,7 +1617,7 @@ def plot_sources( plane = Box(center=center, size=size) source_shapes = self.scene._filter_structures_plane( - structures=structures, plane=plane, property_list=source_list + structures=structures, plane=plane, property_list=source_list, transpose=transpose ) source_min, source_max = self.source_bounds(property=property) @@ -1596,11 +1633,13 @@ def plot_sources( ) # clean up the axis display - ax = self.add_ax_lims(axis=axis, ax=ax) - ax = Scene._set_plot_bounds(bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z) + ax = self.add_ax_lims(axis=axis, ax=ax, transpose=transpose) + ax = Scene._set_plot_bounds( + bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, transpose=transpose + ) # Add the default axis labels, tick labels, and title ax = Box.add_ax_labels_and_title( - ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units + ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units, transpose=transpose ) return ax diff --git a/tidy3d/plugins/microwave/array_factor.py b/tidy3d/plugins/microwave/array_factor.py index ca688b7ee..94c5f38d2 100644 --- a/tidy3d/plugins/microwave/array_factor.py +++ b/tidy3d/plugins/microwave/array_factor.py @@ -359,9 +359,11 @@ def _duplicate_grid_specs( for translation_vector in self._antenna_locations: for snapping_point in grid_spec.snapping_points: new_snapping_point = [ - snapping_point[dim] + translation_vector[dim] - if snapping_point[dim] is not None - else None + ( + snapping_point[dim] + translation_vector[dim] + if snapping_point[dim] is not None + else None + ) for dim in range(3) ] array_snapping_points.append(new_snapping_point) diff --git a/tidy3d/web/core/s3utils.py b/tidy3d/web/core/s3utils.py index 8eba4a1fe..95ea5679f 100644 --- a/tidy3d/web/core/s3utils.py +++ b/tidy3d/web/core/s3utils.py @@ -255,9 +255,9 @@ def _upload(_callback: Callable) -> None: Key=token.get_s3_key(), Callback=_callback, Config=_s3_config, - ExtraArgs={"ContentEncoding": "gzip"} - if token.get_s3_key().endswith(".gz") - else None, + ExtraArgs=( + {"ContentEncoding": "gzip"} if token.get_s3_key().endswith(".gz") else None + ), ) if progress_callback is not None: