diff --git a/tests/test_components/test_eme.py b/tests/test_components/test_eme.py index bf30e940f1..57294344e4 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_heat.py b/tests/test_components/test_heat.py index ccf29af9d6..833297472c 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 @@ -526,21 +527,22 @@ def make_heat_sim_data(): return heat_sim_data -def test_sim_data(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_sim_data(transpose): heat_sim_data = make_heat_sim_data() - _ = heat_sim_data.plot_field("test", z=0) - _ = heat_sim_data.plot_field("tri") - _ = heat_sim_data.plot_field("tet", y=0.5) + _ = heat_sim_data.plot_field("test", z=0, transpose=transpose) + _ = heat_sim_data.plot_field("tri", transpose=transpose) + _ = heat_sim_data.plot_field("tet", y=0.5, transpose=transpose) plt.close() with pytest.raises(DataError): - _ = heat_sim_data.plot_field("empty") + _ = heat_sim_data.plot_field("empty", transpose=transpose) with pytest.raises(DataError): - _ = heat_sim_data.plot_field("test") + _ = heat_sim_data.plot_field("test", transpose=transpose) with pytest.raises(KeyError): - _ = heat_sim_data.plot_field("test3", x=0) + _ = heat_sim_data.plot_field("test3", x=0, transpose=transpose) with pytest.raises(pd.ValidationError): _ = heat_sim_data.updated_copy(data=[heat_sim_data.data[0]] * 2) diff --git a/tests/test_components/test_heat_charge.py b/tests/test_components/test_heat_charge.py index 8c3f823cbf..12927b2050 100644 --- a/tests/test_components/test_heat_charge.py +++ b/tests/test_components/test_heat_charge.py @@ -1026,7 +1026,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, @@ -1057,33 +1058,46 @@ def test_heat_charge_simulation(simulation_data): mesher = mesh_data.mesher assert mesher is not None, "VolumeMesher 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): +@pytest.mark.parametrize("transpose", [True, False]) +def test_sim_data_plotting(transpose: bool, simulation_data): """Tests whether simulation data can be plotted and appropriate errors are raised.""" heat_sim_data, cond_sim_data, cap_sim_data, fc_sim_data, mesh_data = simulation_data # Plotting temperature data - heat_sim_data.plot_field("test", z=0) - heat_sim_data.plot_field("tri") - heat_sim_data.plot_field("tet", y=0.5) + heat_sim_data.plot_field("test", z=0, transpose=transpose) + heat_sim_data.plot_field("tri", transpose=transpose) + heat_sim_data.plot_field("tet", y=0.5, transpose=transpose) # Plotting voltage data - cond_sim_data.plot_field("v_test", z=0) - cond_sim_data.plot_field("v_tri") - cond_sim_data.plot_field("v_tet", y=0.5) + cond_sim_data.plot_field("v_test", z=0, transpose=transpose) + cond_sim_data.plot_field("v_tri", transpose=transpose) + cond_sim_data.plot_field("v_tet", y=0.5, transpose=transpose) plt.close() # Test plotting with no data with pytest.raises(DataError): - heat_sim_data.plot_field("empty") + heat_sim_data.plot_field("empty", transpose=transpose) # Test plotting with 3D data with pytest.raises(DataError): - heat_sim_data.plot_field("test") + heat_sim_data.plot_field("test", transpose=transpose) # Test plotting with invalid key with pytest.raises(KeyError): - heat_sim_data.plot_field("test3", x=0) + heat_sim_data.plot_field("test3", x=0, transpose=transpose) # Test updating simulation data with duplicate data with pytest.raises(pd.ValidationError): @@ -1099,42 +1113,43 @@ def test_sim_data_plotting(simulation_data): heat_sim_data.updated_copy(simulation=sim) -def test_mesh_plotting(simulation_data): +@pytest.mark.parametrize("transpose", [True, False]) +def test_mesh_plotting(transpose: bool, simulation_data): """Tests whether mesh can be plotted and appropriate errors are raised.""" heat_sim_data, cond_sim_data, cap_sim_data, fc_sim_data, mesh_data = simulation_data # Plotting mesh from unstructured temperature data - heat_sim_data.plot_mesh("tri") - heat_sim_data.plot_mesh("tet", y=0.5) + heat_sim_data.plot_mesh("tri", transpose=transpose) + heat_sim_data.plot_mesh("tet", y=0.5, transpose=transpose) # Plotting mesh from unstructured voltage data - cond_sim_data.plot_mesh("v_tri", structures_fill=False) - cond_sim_data.plot_mesh("v_tet", y=0.5) + cond_sim_data.plot_mesh("v_tri", structures_fill=False, transpose=transpose) + cond_sim_data.plot_mesh("v_tet", y=0.5, transpose=transpose) # Plotting mesh from mesh data - mesh_data.plot_mesh("mesh_test", z=0) + mesh_data.plot_mesh("mesh_test", z=0, transpose=transpose) plt.close() # Test plotting from structured data with pytest.raises(DataError): - heat_sim_data.plot_mesh("test") + heat_sim_data.plot_mesh("test", transpose=transpose) # Test plotting with no data with pytest.raises(DataError): - heat_sim_data.plot_mesh("empty") + heat_sim_data.plot_mesh("empty", transpose=transpose) # Test plotting with 3D data with pytest.raises(DataError): - heat_sim_data.plot_mesh("tet") + heat_sim_data.plot_mesh("tet", transpose=transpose) # Test plotting with invalid key with pytest.raises(KeyError): - heat_sim_data.plot_mesh("test3", x=0) + heat_sim_data.plot_mesh("test3", x=0, transpose=transpose) # Test plotting with invalid field_name with pytest.raises(DataError): - mesh_data.plot_mesh("mesh_test", z=0, field_name="wrong") + mesh_data.plot_mesh("mesh_test", z=0, field_name="wrong", transpose=transpose) def test_conduction_simulation_has_conductors(conduction_simulation, structures): @@ -1635,27 +1650,29 @@ def test_dynamic_simulation_updates(heat_simulation): assert updated_sim.monitors[-1].name == "new_temp_mnt" -def test_plotting_functions(simulation_data): +@pytest.mark.parametrize("transpose", [True, False]) +def test_plotting_functions(transpose: bool, simulation_data): """Test plotting functions with various data.""" heat_sim_data, cond_sim_data, cap_sim_data, fc_sim_data, mesh_data = simulation_data # Valid plotting try: - heat_sim_data.plot_field("test", z=0) - cond_sim_data.plot_field("v_test", y=1) + heat_sim_data.plot_field("test", z=0, transpose=transpose) + cond_sim_data.plot_field("v_test", y=1, transpose=transpose) except Exception as e: pytest.fail(f"Plotting raised an exception unexpectedly: {e}") # Invalid field name with pytest.raises(KeyError): - heat_sim_data.plot_field("non_existent_field") + heat_sim_data.plot_field("non_existent_field", transpose=transpose) # Invalid plotting parameters with pytest.raises(KeyError): 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( @@ -1795,8 +1812,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_mode.py b/tests/test_components/test_mode.py index 764a823696..a617f17eb9 100644 --- a/tests/test_components/test_mode.py +++ b/tests/test_components/test_mode.py @@ -173,17 +173,18 @@ def get_mode_sim(): return sim -def test_mode_sim(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_mode_sim(transpose: bool): with AssertLogLevel(None): sim = get_mode_sim() - _ = sim.plot(ax=AX) - _ = sim.plot(ax=AX, fill_structures=False, hlim=(-1, 1), vlim=(-1, 1)) - _ = sim.plot(y=0, ax=AX) - _ = sim.plot_mode_plane(ax=AX) - _ = sim.plot_eps_mode_plane(ax=AX) - _ = sim.plot_structures_eps_mode_plane(ax=AX) - _ = sim.plot_grid_mode_plane(ax=AX) - _ = sim.plot_pml_mode_plane(ax=AX) + _ = sim.plot(ax=AX, transpose=transpose) + _ = sim.plot(ax=AX, fill_structures=False, hlim=(-1, 1), vlim=(-1, 1), transpose=transpose) + _ = sim.plot(y=0, ax=AX, transpose=transpose) + _ = sim.plot_mode_plane(ax=AX, transpose=transpose) + _ = sim.plot_eps_mode_plane(ax=AX, transpose=transpose) + _ = sim.plot_structures_eps_mode_plane(ax=AX, transpose=transpose) + _ = sim.plot_grid_mode_plane(ax=AX, transpose=transpose) + _ = sim.plot_pml_mode_plane(ax=AX, transpose=transpose) _ = sim.reduced_simulation_copy _ = sim.run_local() _ = sim._mode_solver.sim_data @@ -327,9 +328,10 @@ def get_mode_sim_data(): return sim_data -def test_mode_sim_data(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_mode_sim_data(transpose: bool): sim_data = get_mode_sim_data() - _ = sim_data.plot_field("Ey", ax=AX, mode_index=0, f=FS[0]) + _ = sim_data.plot_field("Ey", ax=AX, mode_index=0, f=FS[0], transpose=transpose) def test_plane_crosses_symmetry_plane_warning(monkeypatch): diff --git a/tests/test_components/test_scene.py b/tests/test_components/test_scene.py index 0704b9cdc6..4c57d874d0 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,55 @@ 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) + + +@pytest.mark.parametrize("transpose", [True, False]) +def test_get_structures_2dbox(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) + SCENE._get_structures_2dbox(structures=[s1, s2], z=0.0, 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 dec83baaea..834f16fcf9 100644 --- a/tests/test_components/test_simulation.py +++ b/tests/test_components/test_simulation.py @@ -715,22 +715,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() @@ -828,14 +831,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): @@ -859,15 +862,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 = [] @@ -886,14 +889,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): @@ -919,14 +922,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(): @@ -942,12 +947,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() @@ -956,22 +962,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() @@ -1011,22 +1018,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( @@ -1036,16 +1046,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() @@ -2915,9 +2926,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 b39a1686d2..243caedb3d 100644 --- a/tests/test_components/test_source.py +++ b/tests/test_components/test_source.py @@ -209,19 +209,20 @@ def test_dipole_sources_from_angles(): ) -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( @@ -234,11 +235,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): @@ -264,8 +267,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_data/test_datasets.py b/tests/test_data/test_datasets.py index b3602ff507..4fc9521fde 100644 --- a/tests/test_data/test_datasets.py +++ b/tests/test_data/test_datasets.py @@ -12,9 +12,10 @@ np.random.seed(4) +@pytest.mark.parametrize("transpose", [True, False]) @pytest.mark.parametrize("dataset_type_ind", [0, 1, 2]) @pytest.mark.parametrize("ds_name", ["test123", None]) -def test_triangular_dataset(tmp_path, ds_name, dataset_type_ind, no_vtk=False): +def test_triangular_dataset(tmp_path, transpose, ds_name, dataset_type_ind, no_vtk=False): import tidy3d as td from tidy3d.exceptions import DataError, Tidy3dImportError @@ -241,37 +242,37 @@ def test_triangular_dataset(tmp_path, ds_name, dataset_type_ind, no_vtk=False): if len(extra_dims) > 0: with pytest.raises(DataError): - _ = tri_grid.plot() + _ = tri_grid.plot(transpose=transpose) tri_grid_one_field = tri_grid.isel(**{key: value[0] for key, value in extra_dims.items()}) # plotting - _ = tri_grid_one_field.plot() + _ = tri_grid_one_field.plot(transpose=transpose) plt.close() - _ = tri_grid_one_field.plot(grid=False) + _ = tri_grid_one_field.plot(grid=False, transpose=transpose) plt.close() - _ = tri_grid_one_field.plot(field=False) + _ = tri_grid_one_field.plot(field=False, transpose=transpose) plt.close() - _ = tri_grid_one_field.plot(cbar=False) + _ = tri_grid_one_field.plot(cbar=False, transpose=transpose) plt.close() - _ = tri_grid_one_field.plot(vmin=-20, vmax=100) + _ = tri_grid_one_field.plot(vmin=-20, vmax=100, transpose=transpose) plt.close() - _ = tri_grid_one_field.plot(cbar_kwargs={"label": "test"}) + _ = tri_grid_one_field.plot(cbar_kwargs={"label": "test"}, transpose=transpose) plt.close() - _ = tri_grid_one_field.plot(cmap="RdBu") + _ = tri_grid_one_field.plot(cmap="RdBu", transpose=transpose) plt.close() - _ = tri_grid_one_field.plot(shading="flat") + _ = tri_grid_one_field.plot(shading="flat", transpose=transpose) plt.close() with pytest.raises(DataError): - _ = tri_grid.plot(field=False, grid=False) + _ = tri_grid.plot(field=False, grid=False, transpose=transpose) # generalized selection method if no_vtk: diff --git a/tests/test_data/test_sim_data.py b/tests/test_data/test_sim_data.py index 2ea3da663d..0ae3249a06 100644 --- a/tests/test_data/test_sim_data.py +++ b/tests/test_data/test_sim_data.py @@ -148,8 +148,9 @@ def test_centers(): _ = sim_data.at_centers(mon.name) +@pytest.mark.parametrize("transpose", [True, False]) @pytest.mark.parametrize("phase", [0, 1.0]) -def test_plot(phase): +def test_plot(transpose: bool, phase: float): sim_data = make_sim_data() # plot regular field data @@ -158,16 +159,31 @@ def test_plot(phase): for axis_name in "xyz": xyz_kwargs = {axis_name: field_data.coords[axis_name][0]} _ = sim_data.plot_field( - "field", field_cmp, val="imag", f=1e14, phase=phase, **xyz_kwargs + "field", + field_cmp, + val="imag", + f=1e14, + phase=phase, + transpose=transpose, + **xyz_kwargs, ) plt.close() for shading in ["gouraud", "nearest", "auto"]: _ = sim_data.plot_field( - "field", field_cmp, val="imag", f=1e14, phase=phase, shading=shading, **xyz_kwargs + "field", + field_cmp, + val="imag", + f=1e14, + phase=phase, + shading=shading, + transpose=transpose, + **xyz_kwargs, ) for axis_name in "xyz": xyz_kwargs = {axis_name: 0} - _ = sim_data.plot_field("field", "int", f=1e14, phase=phase, **xyz_kwargs) + _ = sim_data.plot_field( + "field", "int", f=1e14, phase=phase, transpose=transpose, **xyz_kwargs + ) plt.close() # plot field time data @@ -176,34 +192,56 @@ def test_plot(phase): for axis_name in "xyz": xyz_kwargs = {axis_name: field_data.coords[axis_name][0]} _ = sim_data.plot_field( - "field_time", field_cmp, val="real", phase=phase, t=0.0, **xyz_kwargs + "field_time", + field_cmp, + val="real", + phase=phase, + t=0.0, + transpose=transpose, + **xyz_kwargs, ) plt.close() for axis_name in "xyz": xyz_kwargs = {axis_name: 0} - _ = sim_data.plot_field("field_time", "int", t=0.0, phase=phase, **xyz_kwargs) + _ = sim_data.plot_field( + "field_time", "int", t=0.0, phase=phase, transpose=transpose, **xyz_kwargs + ) plt.close() # plot mode field data for field_cmp in ("Ex", "Ey", "Ez", "Hx", "Hy", "Hz"): _ = sim_data.plot_field( - "mode_solver", field_cmp, val="real", f=1e14, mode_index=1, phase=phase + "mode_solver", + field_cmp, + val="real", + f=1e14, + mode_index=1, + phase=phase, + transpose=transpose, ) plt.close() - _ = sim_data.plot_field("mode_solver", "int", f=1e14, mode_index=1, phase=phase) + _ = sim_data.plot_field( + "mode_solver", "int", f=1e14, mode_index=1, phase=phase, transpose=transpose + ) plt.close() -def test_plot_field_missing_derived_data(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_plot_field_missing_derived_data(transpose: bool): sim_data = make_sim_data() with pytest.raises(Tidy3dKeyError): - sim_data.plot_field(field_monitor_name="field_time", field_name="E", val="int") + sim_data.plot_field( + field_monitor_name="field_time", field_name="E", val="int", transpose=transpose + ) -def test_plot_field_missing_field_value(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_plot_field_missing_field_value(transpose: bool): sim_data = make_sim_data() with pytest.raises(Tidy3dKeyError): - sim_data.plot_field(field_monitor_name="field", field_name="Ex", val="test") + sim_data.plot_field( + field_monitor_name="field", field_name="Ex", val="test", transpose=transpose + ) @pytest.mark.parametrize("monitor_name", ["field", "field_time", "mode_solver"]) @@ -247,9 +285,10 @@ def test_to_json(tmp_path): @pytest.mark.filterwarnings("ignore:log10") +@pytest.mark.parametrize("transpose", [True, False]) @pytest.mark.parametrize("field_name", ["Ex", "Ey", "Ez", "E", "Hx", "Hz", "Sy"]) @pytest.mark.parametrize("val", ["real", "re", "imag", "im", "abs", "phase"]) -def test_derived_components(field_name, val): +def test_derived_components(transpose: bool, field_name: str, val: str): sim_data = make_sim_data() if len(field_name) == 1 and val == "phase": with pytest.raises(Tidy3dKeyError): @@ -259,6 +298,7 @@ def test_derived_components(field_name, val): val=val, y=0.0, time=1e-12, + transpose=transpose, ) else: sim_data.plot_field( @@ -267,42 +307,55 @@ def test_derived_components(field_name, val): val=val, y=0.0, time=1e-12, + transpose=transpose, ) plt.close() -def test_logscale(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_logscale(transpose: bool): sim_data = make_sim_data() - sim_data.plot_field("field_time", "Ex", val="real", scale="dB", y=0.0, time=1e-12) + sim_data.plot_field( + "field_time", "Ex", val="real", scale="dB", y=0.0, time=1e-12, transpose=transpose + ) plt.close() -def test_sel_kwarg_freq(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_sel_kwarg_freq(transpose: bool): """Use freq in sel_kwarg, should still work (but warning) for 1.6.x""" sim_data = make_sim_data() - sim_data.plot_field("mode_solver", "Ex", y=0.0, val="real", freq=1e14, mode_index=1) + sim_data.plot_field( + "mode_solver", "Ex", y=0.0, val="real", freq=1e14, mode_index=1, transpose=transpose + ) plt.close() -def test_sel_kwarg_time(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_sel_kwarg_time(transpose: bool): """Use time in sel_kwarg, should still work (but warning) for 1.6.x""" sim_data = make_sim_data() - sim_data.plot_field("field_time", "Ex", y=0.0, val="real", time=1e-12) + sim_data.plot_field("field_time", "Ex", y=0.0, val="real", time=1e-12, transpose=transpose) plt.close() -def test_sel_kwarg_len1(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_sel_kwarg_len1(transpose: bool): sim_data = make_sim_data() # data has no y dimension (only exists at y=0) # passing y=0 sel kwarg should still work - sim_data.plot_field("mode_solver", "Ex", y=0.0, val="real", f=1e14, mode_index=1) + sim_data.plot_field( + "mode_solver", "Ex", y=0.0, val="real", f=1e14, mode_index=1, transpose=transpose + ) plt.close() # passing y=1 sel kwarg should error with pytest.raises(KeyError): - sim_data.plot_field("mode_solver", "Ex", y=-1.0, val="real", f=1e14, mode_index=1) + sim_data.plot_field( + "mode_solver", "Ex", y=-1.0, val="real", f=1e14, mode_index=1, transpose=transpose + ) plt.close() @@ -422,9 +475,10 @@ def test_run_time_lt_start(tmp_path): _ = sim_data[tmnt.name] -def test_plot_field_title(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_plot_field_title(transpose: bool): sim_data = make_sim_data() - ax = sim_data.plot_field("field", "Ey", "real", f=2e14, z=0.10) + ax = sim_data.plot_field("field", "Ey", "real", f=2e14, z=0.10, transpose=transpose) assert "z=0.10" in ax.title.get_text(), "title rendered incorrectly." plt.close() diff --git a/tests/test_plugins/smatrix/test_component_modeler.py b/tests/test_plugins/smatrix/test_component_modeler.py index 74064826d9..84b413c497 100644 --- a/tests/test_plugins/smatrix/test_component_modeler.py +++ b/tests/test_plugins/smatrix/test_component_modeler.py @@ -248,15 +248,17 @@ def test_validate_batch_supplied(tmp_path): ) -def test_plot_sim(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_plot_sim(transpose: bool): modeler = make_component_modeler() - modeler.plot_sim(z=0) + modeler.plot_sim(z=0, transpose=transpose) plt.close() -def test_plot_sim_eps(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_plot_sim_eps(transpose: bool): modeler = make_component_modeler() - modeler.plot_sim_eps(z=0) + modeler.plot_sim_eps(z=0, transpose=transpose) plt.close() diff --git a/tests/test_plugins/test_invdes.py b/tests/test_plugins/test_invdes.py index 0cb2a399fb..b913901087 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_microwave.py b/tests/test_plugins/test_microwave.py index a769ac700b..fc88852a2d 100644 --- a/tests/test_plugins/test_microwave.py +++ b/tests/test_plugins/test_microwave.py @@ -572,7 +572,8 @@ def impedance_of_coaxial_cable(r1, r2, wave_impedance=td.ETA_0): assert np.allclose(Z_calc, Z_analytic, rtol=0.04) -def test_path_integral_plotting(): +@pytest.mark.parametrize("transpose", [True, False]) +def test_path_integral_plotting(transpose: bool): """Test that all types of path integrals correctly plot themselves.""" mean_radius = (COAX_R2 + COAX_R1) * 0.5 @@ -588,12 +589,12 @@ def test_path_integral_plotting(): ) ax = voltage_integral.plot(z=0) - current_integral.plot(z=0, ax=ax) + current_integral.plot(z=0, ax=ax, transpose=transpose) plt.close() # Test off center plotting ax = voltage_integral.plot(z=2) - current_integral.plot(z=2, ax=ax) + current_integral.plot(z=2, ax=ax, transpose=transpose) plt.close() # Plot @@ -610,12 +611,12 @@ def test_path_integral_plotting(): ) ax = voltage_integral.plot(y=0) - current_integral.plot(y=0, ax=ax) + current_integral.plot(y=0, ax=ax, transpose=transpose) plt.close() # Test off center plotting ax = voltage_integral.plot(y=2) - current_integral.plot(y=2, ax=ax) + current_integral.plot(y=2, ax=ax, transpose=transpose) plt.close() diff --git a/tests/test_plugins/test_mode_solver.py b/tests/test_plugins/test_mode_solver.py index 9e52ca06ea..70acd207a9 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 f6f0e8c903..9e781435df 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 ascending 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 ascending 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 ascending 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 ascending 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 ascending 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 ascending 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 + ascending 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/data/sim_data.py b/tidy3d/components/data/sim_data.py index 55de11ecaf..967976362e 100644 --- a/tidy3d/components/data/sim_data.py +++ b/tidy3d/components/data/sim_data.py @@ -24,6 +24,7 @@ from tidy3d.components.source.utils import SourceType from tidy3d.components.structure import Structure from tidy3d.components.types import Ax, Axis, ColormapType, FieldVal, PlotScale, annotate_type +from tidy3d.components.utils import pop_axis_and_swap from tidy3d.components.viz import add_ax_if_none, equal_aspect from tidy3d.constants import C_0, inf from tidy3d.exceptions import DataError, FileError, Tidy3dKeyError @@ -452,6 +453,7 @@ def plot_field_monitor_data( vmax: Optional[float] = None, ax: Ax = None, shading: str = "flat", + transpose: bool = False, **sel_kwargs, ) -> Ax: """Plot the field data for a monitor with simulation plot overlaid. @@ -488,6 +490,8 @@ def plot_field_monitor_data( matplotlib axes to plot on, if not specified, one is created. shading: str = 'flat' Shading argument for Xarray plot method ('flat','nearest','goraud') + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default ascending axis order) sel_kwargs : keyword arguments used to perform ``.sel()`` selection in the monitor data. These kwargs can select over the spatial dimensions (``x``, ``y``, ``z``), frequency or time dimensions (``f``, ``t``) or ``mode_index``, if applicable. @@ -651,6 +655,7 @@ def plot_field_monitor_data( ax=ax, shading=shading, infer_intervals=True if shading == "flat" else False, + transpose=transpose, ) def plot_field( @@ -666,6 +671,7 @@ def plot_field( vmax: Optional[float] = None, ax: Ax = None, shading: str = "flat", + transpose: bool = False, **sel_kwargs, ) -> Ax: """Plot the field data for a monitor with simulation plot overlaid. @@ -703,6 +709,8 @@ def plot_field( matplotlib axes to plot on, if not specified, one is created. shading: str = 'flat' Shading argument for Xarray plot method ('flat','nearest','goraud') + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default ascending axis order) sel_kwargs : keyword arguments used to perform ``.sel()`` selection in the monitor data. These kwargs can select over the spatial dimensions (``x``, ``y``, ``z``), frequency or time dimensions (``f``, ``t``) or ``mode_index``, if applicable. @@ -730,6 +738,7 @@ def plot_field( vmax=vmax, ax=ax, shading=shading, + transpose=transpose, **sel_kwargs, ) @@ -747,6 +756,7 @@ def plot_scalar_array( vmax: Optional[float] = None, cmap_type: ColormapType = "divergent", ax: Ax = None, + transpose: bool = False, **kwargs, ) -> Ax: """Plot the field data for a monitor with simulation plot overlaid. @@ -780,6 +790,8 @@ def plot_scalar_array( Type of color map to use for plotting. 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 ascending axis order) **kwargs : Extra arguments to ``DataArray.plot``. Returns @@ -807,13 +819,11 @@ def plot_scalar_array( eps_reverse = False # plot the field - xy_coord_labels = list("xyz") - xy_coord_labels.pop(axis) - x_coord_label, y_coord_label = xy_coord_labels[0], xy_coord_labels[1] + _, xy_coord_labels = pop_axis_and_swap(list("xyz"), axis, transpose=transpose) field_data.plot( ax=ax, - x=x_coord_label, - y=y_coord_label, + x=xy_coord_labels[0], + y=xy_coord_labels[1], cmap=cmap, vmin=vmin, vmax=vmax, @@ -830,12 +840,13 @@ def plot_scalar_array( alpha=eps_alpha, reverse=eps_reverse, ax=ax, + transpose=transpose, **interp_kwarg, ) # set the limits based on the xarray coordinates min and max - x_coord_values = field_data.coords[x_coord_label] - y_coord_values = field_data.coords[y_coord_label] + x_coord_values = field_data.coords[xy_coord_labels[0]] + y_coord_values = field_data.coords[xy_coord_labels[1]] ax.set_xlim(min(x_coord_values), max(x_coord_values)) ax.set_ylim(min(y_coord_values), max(y_coord_values)) diff --git a/tidy3d/components/eme/simulation.py b/tidy3d/components/eme/simulation.py index 455ec221c8..55cd66de7e 100644 --- a/tidy3d/components/eme/simulation.py +++ b/tidy3d/components/eme/simulation.py @@ -25,6 +25,7 @@ validate_boundaries_for_zero_dims, ) from tidy3d.components.types import Ax, Axis, FreqArray, Symmetry, annotate_type +from tidy3d.components.utils import pop_axis_and_swap from tidy3d.components.validators import MIN_FREQUENCY, validate_freqs_min, validate_freqs_not_empty from tidy3d.components.viz import add_ax_if_none, equal_aspect from tidy3d.constants import C_0, inf @@ -306,6 +307,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 +317,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) = 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) = pop_axis_and_swap( + self.simulation_bounds[0], axis=axis, transpose=transpose + ) + _, (xmax, ymax) = 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 +340,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 +363,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 +377,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) = 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) = pop_axis_and_swap( + self.simulation_bounds[0], axis=axis, transpose=transpose + ) + _, (xmax, ymax) = 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 +400,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 +435,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 +443,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) = 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) = pop_axis_and_swap( + self.simulation_bounds[0], axis=axis, transpose=transpose + ) + _, (xmax, ymax) = 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 +466,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 +490,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 +513,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 ascending axis order.) Returns ------- @@ -475,23 +523,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/grid/grid.py b/tidy3d/components/grid/grid.py index 5baf3828e0..0f0d7d6397 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/data/sim_data.py b/tidy3d/components/mode/data/sim_data.py index aed1efde1a..3fee929f24 100644 --- a/tidy3d/components/mode/data/sim_data.py +++ b/tidy3d/components/mode/data/sim_data.py @@ -50,6 +50,7 @@ def plot_field( vmin: Optional[float] = None, vmax: Optional[float] = None, ax: Ax = None, + transpose: bool = False, **sel_kwargs, ) -> Ax: """Plot the field for a :class:`.ModeSolverData` with :class:`.Simulation` plot overlaid. @@ -77,6 +78,8 @@ def plot_field( inferred from the data and other keyword arguments. 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 ascending axis order) sel_kwargs : keyword arguments used to perform ``.sel()`` selection in the monitor data. These kwargs can select over the spatial dimensions (``x``, ``y``, ``z``), frequency or time dimensions (``f``, ``t``) or `mode_index`, if applicable. @@ -99,5 +102,6 @@ def plot_field( vmin=vmin, vmax=vmax, ax=ax, + transpose=transpose, **sel_kwargs, ) diff --git a/tidy3d/components/mode/mode_solver.py b/tidy3d/components/mode/mode_solver.py index 6dcc89e27d..0671ef00f6 100644 --- a/tidy3d/components/mode/mode_solver.py +++ b/tidy3d/components/mode/mode_solver.py @@ -55,6 +55,7 @@ PlotScale, Symmetry, ) +from tidy3d.components.utils import pop_axis_and_swap from tidy3d.components.validators import ( validate_freqs_min, validate_freqs_not_empty, @@ -322,14 +323,18 @@ 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 = pop_axis_and_swap(mode_symmetry, axis=normal_axis, transpose=transpose) return solver_sym @cached_property @@ -344,6 +349,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. @@ -355,6 +361,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 ascending axis order.) Returns ------- @@ -364,7 +372,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: @@ -463,13 +473,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) @@ -2070,6 +2086,7 @@ def plot_field( vmin: Optional[float] = None, vmax: Optional[float] = None, ax: Ax = None, + transpose: bool = False, **sel_kwargs, ) -> Ax: """Plot the field for a :class:`.ModeSolverData` with :class:`.Simulation` plot overlaid. @@ -2097,6 +2114,8 @@ def plot_field( inferred from the data and other keyword arguments. 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 ascending axis order) sel_kwargs : keyword arguments used to perform ``.sel()`` selection in the monitor data. These kwargs can select over the spatial dimensions (``x``, ``y``, ``z``), frequency or time dimensions (``f``, ``t``) or `mode_index`, if applicable. @@ -2121,6 +2140,7 @@ def plot_field( vmin=vmin, vmax=vmax, ax=ax, + transpose=transpose, **sel_kwargs, ) @@ -2130,6 +2150,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 the mode plane simulation's components. @@ -2144,6 +2165,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 ascending axis order.) Returns ------- @@ -2159,7 +2182,7 @@ def plot( """ # Get the mode plane normal axis, center, and limits. a_center, hlim_plane, vlim_plane, _ = self._center_and_lims( - simulation=self.simulation, plane=self.plane + simulation=self.simulation, plane=self.plane, transpose=transpose ) if hlim is None: @@ -2178,16 +2201,18 @@ def plot( lumped_element_alpha=0, ax=ax, fill_structures=fill_structures, + 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. @@ -2202,6 +2227,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 ascending axis order.) Returns ------- @@ -2217,7 +2244,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. @@ -2235,6 +2262,7 @@ def plot_eps( monitor_alpha=0, lumped_element_alpha=0, ax=ax, + transpose=transpose, ) def plot_structures_eps( @@ -2244,6 +2272,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. @@ -2263,6 +2292,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 ascending axis order.) Returns ------- @@ -2278,7 +2309,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. @@ -2295,11 +2326,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. @@ -2308,6 +2341,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 ascending axis order.) **kwargs Optional keyword arguments passed to the matplotlib ``LineCollection``. For details on accepted values, refer to @@ -2321,20 +2356,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 @@ -2344,10 +2393,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] @@ -2358,23 +2411,28 @@ 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], ]: """Thickness of the mode solver pml in the form ((plus0, minus0), (plus1, minus1)) - where 0 and 1 are the lexicographically-ordered tangential axes - to the mode plane. + where 0 and 1 are the tangential axes to the mode plane (in ascending order). """ # 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. @@ -2398,20 +2456,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 @@ -2419,6 +2488,7 @@ def _mode_plane_size_no_pml( def plot_pml( self, ax: Ax = None, + transpose: bool = False, ) -> Ax: """Plot the mode plane absorbing boundaries. @@ -2426,6 +2496,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 ascending axis order.) Returns ------- @@ -2433,12 +2505,21 @@ 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.""" @@ -2446,7 +2527,9 @@ def _plot_pml( from matplotlib.patches import Rectangle # 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: @@ -2459,11 +2542,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: @@ -2494,16 +2579,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 = 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) = pop_axis_and_swap( + simulation.bounds[0], axis=n_axis, transpose=transpose + ) + _, (h_max_s, v_max_s) = 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/mode/simulation.py b/tidy3d/components/mode/simulation.py index 9a0300c148..c4ff645c10 100644 --- a/tidy3d/components/mode/simulation.py +++ b/tidy3d/components/mode/simulation.py @@ -433,6 +433,7 @@ def plot( def plot_mode_plane( self, ax: Ax = None, + transpose: bool = False, **patch_kwargs, ) -> Ax: """Plot the mode plane simulation's components. @@ -446,6 +447,8 @@ def plot_mode_plane( ------- matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default ascending axis order) See Also --------- @@ -454,13 +457,14 @@ def plot_mode_plane( * `Visualizing geometries in Tidy3D: Plotting Materials <../../notebooks/VizSimulation.html#Plotting-Materials>`_ """ - return self._mode_solver.plot(ax=ax, **patch_kwargs) + return self._mode_solver.plot(ax=ax, transpose=transpose, **patch_kwargs) def plot_eps_mode_plane( 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. @@ -475,6 +479,8 @@ def plot_eps_mode_plane( 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 ascending axis order) Returns ------- @@ -487,7 +493,7 @@ def plot_eps_mode_plane( **Notebooks** * `Visualizing geometries in Tidy3D: Plotting Permittivity <../../notebooks/VizSimulation.html#Plotting-Permittivity>`_ """ - return self._mode_solver.plot_eps(freq=freq, alpha=alpha, ax=ax) + return self._mode_solver.plot_eps(freq=freq, alpha=alpha, ax=ax, transpose=transpose) def plot_structures_eps_mode_plane( self, @@ -496,6 +502,7 @@ def plot_structures_eps_mode_plane( 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. @@ -515,6 +522,8 @@ def plot_structures_eps_mode_plane( 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 ascending axis order) Returns ------- @@ -528,12 +537,13 @@ def plot_structures_eps_mode_plane( * `Visualizing geometries in Tidy3D: Plotting Permittivity <../../notebooks/VizSimulation.html#Plotting-Permittivity>`_ """ return self._mode_solver.plot_structures_eps( - freq=freq, alpha=alpha, cbar=cbar, reverse=reverse, ax=ax + freq=freq, alpha=alpha, cbar=cbar, reverse=reverse, ax=ax, transpose=transpose ) def plot_grid_mode_plane( self, ax: Ax = None, + transpose: bool = False, **kwargs, ) -> Ax: """Plot the mode plane cell boundaries as lines. @@ -542,6 +552,8 @@ def plot_grid_mode_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 ascending axis order) **kwargs Optional keyword arguments passed to the matplotlib ``LineCollection``. For details on accepted values, refer to @@ -552,11 +564,12 @@ def plot_grid_mode_plane( matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ - return self._mode_solver.plot_grid(ax=ax) + return self._mode_solver.plot_grid(ax=ax, transpose=transpose) def plot_pml_mode_plane( self, ax: Ax = None, + transpose: bool = False, ) -> Ax: """Plot the mode plane absorbing boundaries. @@ -564,13 +577,15 @@ def plot_pml_mode_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 ascending axis order) Returns ------- matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ - return self._mode_solver.plot_pml(ax=ax) + return self._mode_solver.plot_pml(ax=ax, transpose=transpose) def validate_pre_upload(self, source_required: bool = False): self._mode_solver.validate_pre_upload(source_required=source_required) diff --git a/tidy3d/components/monitor.py b/tidy3d/components/monitor.py index 1ad5cef331..c257e9ea6b 100644 --- a/tidy3d/components/monitor.py +++ b/tidy3d/components/monitor.py @@ -358,12 +358,13 @@ 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) + # call the monitor.plot() function first + 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 83abbcf330..ee7c6c4ae2 100644 --- a/tidy3d/components/scene.py +++ b/tidy3d/components/scene.py @@ -22,6 +22,12 @@ from tidy3d.components.material.types import MultiPhysicsMediumType3D, StructureMediumType from tidy3d.components.tcad.doping import ConstantDoping, GaussianDoping from tidy3d.components.tcad.viz import HEAT_SOURCE_CMAP +from tidy3d.components.utils import ( + pop_axis_and_swap, + shape_swap_xy, + unpop_axis_and_swap, + warn_untested_argument, +) from tidy3d.constants import CONDUCTIVITY, THERMAL_CONDUCTIVITY, inf from tidy3d.exceptions import SetupError, Tidy3dError from tidy3d.log import log @@ -282,7 +288,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 +302,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 +313,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 +329,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 +342,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 ------- @@ -366,11 +382,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) = pop_axis_and_swap(bounds[0], axis=axis, transpose=transpose) + _, (hmax, vmax) = pop_axis_and_swap(bounds[1], axis=axis, transpose=transpose) # account for unordered limits if hlim is None: @@ -403,6 +420,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. @@ -423,17 +441,24 @@ 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 ascending axis order.) Returns ------- matplotlib.axes._subplots.Axes 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, transpose=transpose + ) - hlim, vlim = Scene._get_plot_lims(bounds=self.bounds, 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) - 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 @@ -447,6 +472,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. @@ -466,15 +492,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 ascending 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: @@ -485,15 +518,18 @@ def plot_structures( shape=shape, ax=ax, fill=fill, + transpose=False, # _get_structures_2dbox() already took care of transposing shape ) # 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 @@ -504,6 +540,7 @@ def _plot_shape_structure( shape: Shapely, ax: Ax, fill: bool = True, + transpose: bool = False, ) -> Ax: """Plot a structure's cross section shape for a given medium.""" plot_params_struct = self._get_structure_plot_params( @@ -511,7 +548,9 @@ def _plot_shape_structure( mat_index=mat_index, fill=fill, ) - ax = self.box.plot_shape(shape=shape, plot_params=plot_params_struct, ax=ax) + ax = self.box.plot_shape( + shape=shape, plot_params=plot_params_struct, ax=ax, transpose=transpose + ) return ax def _get_structure_plot_params( @@ -594,6 +633,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. @@ -611,13 +651,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 ascending 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 @@ -630,6 +674,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). @@ -647,6 +692,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 ascending axis order.) Returns ------- @@ -655,8 +702,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) = pop_axis_and_swap(self.bounds[0], axis=axis, transpose=transpose) + _, (hmax, vmax) = pop_axis_and_swap(self.bounds[1], axis=axis, transpose=transpose) if hlim is not None: (hmin, hmax) = hlim @@ -670,8 +717,10 @@ 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 = unpop_axis_and_swap( + center_normal, (h_center, v_center), axis=axis, transpose=transpose + ) + size = unpop_axis_and_swap(0.0, (h_size, v_size), axis=axis, transpose=transpose) plane = Box(center=center, size=size) medium_shapes = [] @@ -680,12 +729,16 @@ def _get_structures_2dbox( for shape in intersections: if not shape.is_empty: shape = Box.evaluate_inf_shape(shape) + if transpose: + shape = shape_swap_xy(shape) medium_shapes.append((structure.medium, shape)) return medium_shapes @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. @@ -696,16 +749,17 @@ 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 ------- List[Tuple[:class:`.AbstractMedium`, shapely.geometry.base.BaseGeometry]] List of shapes and mediums on the plane after merging. """ - 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 @@ -713,6 +767,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. @@ -725,6 +780,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 ------- @@ -732,7 +789,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 """ @@ -749,6 +809,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. @@ -773,6 +834,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 ascending axis order.) Returns ------- @@ -780,12 +843,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 @@ -805,6 +881,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. @@ -840,13 +917,14 @@ 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 ascending axis order.) Returns ------- matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ - return self.plot_structures_property( x=x, y=y, @@ -862,6 +940,7 @@ def plot_structures_eps( grid=grid, property="eps", eps_component=eps_component, + transpose=transpose, ) @equal_aspect @@ -882,9 +961,10 @@ 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. + The selected property is plotted in grayscale based on its value at the specified frequency. Parameters ---------- @@ -920,13 +1000,14 @@ 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 ascending axis order.) Returns ------- matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ - structures = self.sorted_structures # alpha is None just means plot without any transparency @@ -951,11 +1032,21 @@ 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 @@ -993,10 +1084,22 @@ def plot_structures_property( shape=shape, ax=ax, property="doping", + # Note: I omitted `transpose` because we took care of that earlier. ) else: self._pcolormesh_shape_doping_box( - x, y, z, alpha, medium, property_min, property_max, shape, ax, property + x, + y, + z, + alpha, + medium, + property_min, + property_max, + shape, + ax, + property, + transpose=transpose, + transpose_shape=False, # We took care of transposing the shape earlier. ) else: # if the background medium is custom medium, it needs to be rendered separately @@ -1014,6 +1117,7 @@ def plot_structures_property( shape=shape, ax=ax, eps_component=eps_component, + # Note: I omitted `transpose` because we took care of that earlier. ) else: # For custom medium, apply pcolormesh clipped by the shape. @@ -1031,6 +1135,8 @@ def plot_structures_property( ax, grid, eps_component=eps_component, + transpose=transpose, + transpose_shape=False, # We took care of transposing the "shape" earlier. ) if cbar: @@ -1050,10 +1156,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 @@ -1127,26 +1235,48 @@ def _pcolormesh_shape_custom_medium_structure_eps( ax: Ax, grid: Grid, eps_component: Optional[PermittivityComponent] = None, + transpose: bool = False, + transpose_shape: Optional[bool] = None, ): """ Plot shape made of custom medium with ``pcolormesh``. """ + if transpose: + # Please remove this warning once someone has verified that `transpose=True` works. + warn_untested_argument( + cls_name=type(self).__name__, + func_name="_pcolormesh_shape_custom_medium_structure_eps", + arg="transpose", + val="True", + ) + coords = "xyz" normal_axis_ind, normal_position = Box.parse_xyz_kwargs(x=x, y=y, z=z) - normal_axis, plane_axes = Box.pop_axis(coords, normal_axis_ind) + normal_axis, plane_axes = pop_axis_and_swap(coords, normal_axis_ind, transpose=transpose) comp2ind = {dim + dim: index for dim, index in zip("xyz", range(3))} # make grid for eps interpolation # we will do this by combining shape bounds and points where custom eps is provided + if transpose_shape is None: + transpose_shape = transpose # by default, transpose shape if you transform the meshgrid + if transpose_shape: + shape = shape_swap_xy(shape) shape_bounds = shape.bounds rmin, rmax = [*shape_bounds[:2]], [*shape_bounds[2:]] + if transpose: + rmin.reverse() + rmax.reverse() + # Implementation Details: If transpose==True, then the "shape" argument + # will have already been modified by the caller (swapping its horizontal + # and vertical components), before it is passed to this function. rmin.insert(normal_axis_ind, normal_position) rmax.insert(normal_axis_ind, normal_position) if grid is None: - plane_axes_inds = [0, 1, 2] - plane_axes_inds.pop(normal_axis_ind) + _, plane_axes_inds = pop_axis_and_swap( + [0, 1, 2], axis=normal_axis_ind, transpose=transpose + ) eps_diag = medium.eps_dataarray_freq(frequency=freq) @@ -1193,6 +1323,7 @@ def _pcolormesh_shape_custom_medium_structure_eps( "clip_box": ax.bbox, "alpha": alpha, }, + transpose=transpose, ) return @@ -1263,6 +1394,9 @@ def _pcolormesh_shape_custom_medium_structure_eps( # pcolormesh plane_xp, plane_yp = np.meshgrid(plane_coord[0], plane_coord[1], indexing="ij") + if transpose: + plane_yp, plane_xp = np.meshgrid(plane_coord[1], plane_coord[0], indexing="ij") + ax.pcolormesh( plane_xp, plane_yp, @@ -1329,6 +1463,7 @@ def _plot_shape_structure_eps( reverse: bool = False, alpha: Optional[float] = None, eps_component: Optional[PermittivityComponent] = None, + transpose: bool = False, ) -> Ax: """Plot a structure's cross section shape for a given medium, grayscale for permittivity.""" plot_params = self._get_structure_eps_plot_params( @@ -1340,7 +1475,7 @@ def _plot_shape_structure_eps( reverse=reverse, eps_component=eps_component, ) - ax = self.box.plot_shape(shape=shape, plot_params=plot_params, ax=ax) + ax = self.box.plot_shape(shape=shape, plot_params=plot_params, ax=ax, transpose=transpose) return ax """ Plotting Heat """ @@ -1358,8 +1493,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 @@ -1384,19 +1520,33 @@ 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 ascending axis order.) Returns ------- matplotlib.axes._subplots.Axes 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 @@ -1412,6 +1562,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. @@ -1438,13 +1589,14 @@ 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 ascending axis order.) Returns ------- matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ - log.warning( "This function 'plot_structures_heat_conductivity' is deprecated and " "will be discontinued. In its place you can use " @@ -1462,6 +1614,7 @@ def plot_structures_heat_conductivity( ax=ax, hlim=hlim, vlim=vlim, + transpose=transpose, ) @equal_aspect @@ -1478,6 +1631,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. @@ -1504,13 +1658,14 @@ 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 ascending axis order.) Returns ------- matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ - structures = self.sorted_structures # alpha is None just means plot without any transparency @@ -1525,11 +1680,19 @@ 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,6 +1706,7 @@ def plot_structures_heat_charge_property( shape=shape, ax=ax, property=property, + # Note: I omitted `transpose` because we took care of that earlier. ) if cbar: @@ -1562,10 +1726,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 @@ -1668,6 +1834,7 @@ def _plot_shape_structure_heat_charge_property( ax: Ax, reverse: bool = False, alpha: Optional[float] = None, + transpose: bool = False, ) -> Ax: """Plot a structure's cross section shape for a given medium, grayscale for thermal conductivity. @@ -1680,7 +1847,7 @@ def _plot_shape_structure_heat_charge_property( reverse=reverse, property=property, ) - ax = self.box.plot_shape(shape=shape, plot_params=plot_params, ax=ax) + ax = self.box.plot_shape(shape=shape, plot_params=plot_params, ax=ax, transpose=transpose) return ax @equal_aspect @@ -1695,6 +1862,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. @@ -1718,19 +1886,19 @@ 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 ascending axis order.) Returns ------- matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ - log.warning( "The function 'plot_heat_conductivity' is deprecated and will be " "discontinued. In its place you can use " 'plot_heat_charge_property(property="heat_conductivity")' ) - return self.plot_heat_charge_property( x=x, y=y, @@ -1741,6 +1909,7 @@ def plot_heat_conductivity( ax=ax, hlim=hlim, vlim=vlim, + transpose=transpose, ) """ Misc """ @@ -1884,31 +2053,40 @@ def _pcolormesh_shape_doping_box( shape: Shapely, ax: Ax, plt_type: str = "doping", + transpose: bool = False, + transpose_shape: Optional[bool] = None, ): """ Plot shape made of structure defined with doping. plt_type accepts ["doping", "N_a", "N_d"] """ - coords = "xyz" normal_axis_ind, normal_position = Box.parse_xyz_kwargs(x=x, y=y, z=z) - normal_axis, plane_axes = Box.pop_axis(coords, normal_axis_ind) # make grid for eps interpolation - # we will do this by combining shape bounds and points where custom eps is provided + # we will do this by combining shape bounds and points where custom properties are provided + if transpose_shape is None: + transpose_shape = transpose # by default, transpose shape if you transform the meshgrid + if transpose_shape: + shape = shape_swap_xy(shape) shape_bounds = shape.bounds + rmin, rmax = [*shape_bounds[:2]], [*shape_bounds[2:]] rmin.insert(normal_axis_ind, normal_position) rmax.insert(normal_axis_ind, normal_position) # for the time being let's assume we'll always need to generate a mesh - plane_axes_inds = [0, 1, 2] - plane_axes_inds.pop(normal_axis_ind) + _, plane_axes_inds = pop_axis_and_swap([0, 1, 2], axis=normal_axis_ind, transpose=transpose) # build grid N = 100 + coords_2D = [np.linspace(rmin[d], rmax[d], N) for d in plane_axes_inds] X, Y = np.meshgrid(coords_2D[0], coords_2D[1], indexing="ij") + if transpose: + X, Y = Y, X # the mesh coordinates that will be displayed to the user + coords_2D.reverse() # = [coords_2D[1], coords_2D[0]] used for lookup + struct_doping = [ np.zeros(X.shape), # let's use 0 for N_a np.zeros(X.shape), # and 1 for N_d diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index 8e745d7830..3ff41cdb10 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -19,6 +19,7 @@ import pydantic.v1 as pydantic import xarray as xr +from tidy3d.components.utils import pop_axis_and_swap from tidy3d.constants import C_0, SECOND, fp_eps, inf from tidy3d.exceptions import SetupError, Tidy3dError, Tidy3dImportError, ValidationError from tidy3d.log import log @@ -464,6 +465,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. @@ -490,6 +492,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 ascending axis order.) Returns ------- @@ -504,9 +508,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, @@ -515,20 +518,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 @@ -548,6 +568,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. @@ -586,6 +607,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 ascending axis order.) Returns ------- @@ -609,7 +632,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( @@ -624,18 +647,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 @@ -654,6 +696,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. @@ -691,6 +734,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 ascending axis order.) Returns ------- @@ -699,7 +744,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] @@ -727,6 +772,7 @@ def plot_structures_eps( reverse=reverse, eps_component=eps_component, eps_lim=eps_lim, + transpose=transpose, ) @equal_aspect @@ -739,6 +785,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. @@ -757,6 +804,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 ascending axis order.) Returns ------- @@ -766,13 +815,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 @@ -897,6 +953,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. @@ -917,6 +974,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 ascending axis order.) Returns ------- @@ -926,9 +985,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 @@ -943,6 +1011,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. @@ -965,6 +1034,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 ascending axis order.) **kwargs Optional keyword arguments passed to the matplotlib ``LineCollection``. For details on accepted values, refer to @@ -983,7 +1054,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) = 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]] @@ -1019,7 +1090,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)) = pop_axis_and_swap( + bounds, axis=axis, transpose=transpose + ) xmin, xmax, ymin, ymax = ( self._evaluate_inf(v) for v in (xmin, xmax, ymin, ymax) ) @@ -1042,7 +1115,7 @@ def plot_grid( plot_params, ): for point in points: - _, (x_point, y_point) = Geometry.pop_axis(point, axis=axis) + _, (x_point, y_point) = 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: @@ -1071,11 +1144,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 @@ -1087,6 +1167,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 @@ -1102,6 +1183,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 ascending axis order.) **kwargs Optional keyword arguments passed to the matplotlib ``LineCollection``. For details on accepted values, refer to @@ -1137,7 +1220,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) = pop_axis_and_swap([0, 1, 2], axis=normal_axis, transpose=transpose) umin, umax = ax.get_xlim() vmin, vmax = ax.get_ylim() @@ -1190,12 +1273,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 388896fb5b..d81b06bde7 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 a67bf019e9..16fa4d8a8b 100644 --- a/tidy3d/components/source/field.py +++ b/tidy3d/components/source/field.py @@ -710,9 +710,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/tcad/bandgap.py b/tidy3d/components/tcad/bandgap.py index 55c2cedafc..e76557ea9c 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 e46840a1e0..a74d9732d1 100644 --- a/tidy3d/components/tcad/data/sim_data.py +++ b/tidy3d/components/tcad/data/sim_data.py @@ -7,6 +7,7 @@ import numpy as np import pydantic.v1 as pd +from xarray import DataArray as XrDataArray from tidy3d.components.base import Tidy3dBaseModel from tidy3d.components.base_sim.data.sim_data import AbstractSimulationData @@ -30,6 +31,7 @@ from tidy3d.components.tcad.simulation.heat import HeatSimulation from tidy3d.components.tcad.simulation.heat_charge import HeatChargeSimulation from tidy3d.components.types import Ax, RealFieldVal, annotate_type +from tidy3d.components.utils import pop_axis_and_swap, warn_untested_argument from tidy3d.components.viz import add_ax_if_none, equal_aspect from tidy3d.exceptions import DataError, Tidy3dKeyError from tidy3d.log import log @@ -120,6 +122,7 @@ def plot_mesh( field_name: Optional[str] = None, structures_fill: bool = True, ax: Ax = None, + transpose: bool = False, **sel_kwargs, ) -> Ax: """Plot the simulation mesh in a monitor region with structures overlaid. @@ -134,6 +137,8 @@ def plot_mesh( Whether to overlay the mesh on structures filled with color or only show structure outlines. 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 ascending axis order) sel_kwargs : keyword arguments used to perform ``.sel()`` selection in the monitor data. These kwargs can select over the spatial dimensions (``x``, ``y``, ``z``), or time dimension (``t``) if applicable. @@ -151,6 +156,14 @@ def plot_mesh( matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ + if transpose: + # Please remove this warning once someone has verified that `transpose=True` works. + warn_untested_argument( + cls_name=type(self).__name__, + func_name="plot_mesh", + arg="transpose", + val="True", + ) monitor_data = self[monitor_name] @@ -174,11 +187,8 @@ def plot_mesh( position = field_data.normal_pos # 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 = pop_axis_and_swap(field_data.bounds[0], axis, transpose=transpose) + _, max_bounds = pop_axis_and_swap(field_data.bounds[1], axis, transpose=transpose) # select the cross section data interp_kwarg = {"xyz"[axis]: position} @@ -188,11 +198,35 @@ def plot_mesh( fill=structures_fill, hlim=(min_bounds[0], max_bounds[0]), vlim=(min_bounds[1], max_bounds[1]), + transpose=transpose, **interp_kwarg, ) - # only then overlay the mesh plot - field_data.plot(ax=ax, cmap=False, field=False, grid=True) + if not transpose: + field_data.plot(ax=ax, field=False, grid=True) + # Otherwise, we must handle `transpose=True` case differently depending on type(field_data). + # (Not all versions of `.plot()` understand the `transpose` argument.) + elif isinstance(field_data, TriangularGridDataset): + field_data.plot( + ax=ax, + field=False, + grid=True, + transpose=transpose, + ) + elif isinstance(field_data, XrDataArray): + _, xy_coord_labels = pop_axis_and_swap(list("xyz"), axis, transpose=transpose) + field_data.plot( + ax=ax, + x=xy_coord_labels[0], + y=xy_coord_labels[1], + field=False, + grid=True, + ) + else: + raise NotImplementedError( + "`AbstractHeatChargeSimulationData.plot_mesh()` does not support " + f"{type(field_data).__name__} data when `transpose=True`." + ) # set the limits based on the xarray coordinates min and max ax.set_xlim(min_bounds[0], max_bounds[0]) @@ -272,6 +306,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 structures overlaid. @@ -301,6 +336,8 @@ def plot_field( inferred from the data and other keyword arguments. 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 ascending axis order) sel_kwargs : keyword arguments used to perform ``.sel()`` selection in the monitor data. These kwargs can select over the spatial dimensions (``x``, ``y``, ``z``), or time dimension (``t``) if applicable. @@ -357,6 +394,7 @@ def plot_field( vmax=vmax, cbar_kwargs={"label": field_name}, grid=False, + transpose=transpose, ) # compute parameters for structures overlay plot @@ -364,11 +402,8 @@ def plot_field( position = field_data.normal_pos # 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 = pop_axis_and_swap(field_data.bounds[0], axis, transpose=transpose) + _, max_bounds = pop_axis_and_swap(field_data.bounds[1], axis, transpose=transpose) if isinstance(field_data, SpatialDataArray): # interp out any monitor.size==0 dimensions @@ -419,14 +454,11 @@ def plot_field( planar_coord = [name for name, c in spatial_coords_in_data.items() if c is False][0] axis = "xyz".index(planar_coord) position = float(field_data.coords[planar_coord]) - - xy_coord_labels = list("xyz") - xy_coord_labels.pop(axis) - x_coord_label, y_coord_label = xy_coord_labels[0], xy_coord_labels[1] + _, xy_coord_labels = pop_axis_and_swap(list("xyz"), axis=axis, transpose=transpose) field_data.plot( ax=ax, - x=x_coord_label, - y=y_coord_label, + x=xy_coord_labels[0], + y=xy_coord_labels[1], cmap=cmap, vmin=vmin, vmax=vmax, @@ -435,8 +467,8 @@ def plot_field( ) # compute plot bounds - x_coord_values = field_data.coords[x_coord_label] - y_coord_values = field_data.coords[y_coord_label] + x_coord_values = field_data.coords[xy_coord_labels[0]] + y_coord_values = field_data.coords[xy_coord_labels[1]] min_bounds = (min(x_coord_values), min(y_coord_values)) max_bounds = (max(x_coord_values), max(y_coord_values)) @@ -449,6 +481,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 ace1d8a855..16a4f08456 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 ascending 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 7b4b693c74..6abce74921 100644 --- a/tidy3d/components/tcad/simulation/heat_charge.py +++ b/tidy3d/components/tcad/simulation/heat_charge.py @@ -1,4 +1,4 @@ -# ruff: noqa: W293, W291 +# ruff: noqa: W291 """Defines heat simulation class""" from __future__ import annotations @@ -81,6 +81,7 @@ plot_params_heat_source, ) from tidy3d.components.types import TYPE_TAG_STR, Ax, Bound, ScalarSymmetry, Shapely, annotate_type +from tidy3d.components.utils import shape_swap_xy from tidy3d.components.viz import PlotParams, add_ax_if_none, equal_aspect from tidy3d.constants import VOLUMETRIC_HEAT_RATE, inf from tidy3d.exceptions import SetupError @@ -982,6 +983,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. @@ -1009,15 +1011,16 @@ 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 ascending axis order.) Returns ------- matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ - 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 @@ -1052,16 +1055,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) @@ -1081,6 +1102,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: """ @@ -1111,6 +1133,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 ascending axis order.) Returns ------- @@ -1140,6 +1164,7 @@ def plot_heat_conductivity( property=plot_type, hlim=hlim, vlim=vlim, + transpose=transpose, ) @equal_aspect @@ -1151,6 +1176,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. @@ -1168,13 +1194,14 @@ 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 ascending axis order.) Returns ------- matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ - # get structure list structures = [self.simulation_structure] structures += list(self.scene.sorted_structures) @@ -1190,6 +1217,7 @@ def plot_boundaries( structures=structures, plane=plane, boundary_spec=self.boundary_spec, + transpose=transpose, ) # plot boundary conditions @@ -1204,11 +1232,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 @@ -1465,6 +1495,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. @@ -1476,6 +1507,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 ascending axis order.) Returns ------- @@ -1491,6 +1524,8 @@ def _construct_heat_charge_boundaries( # append each of them and their medium information to the list of shapes for shape in shapes_plane: + if transpose: + shape = shape_swap_xy(shape) shapes.append((structure.name, structure.medium, shape, shape.bounds)) background_structure_shape = shapes[0][2] @@ -1537,6 +1572,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. @@ -1553,19 +1589,22 @@ def plot_sources( Options are ["heat_conductivity", "electric_conductivity"] hlim : Tuple[float, float] = None The x range if plotting on xy or xz planes, y range if plotting on yz plane. + (WARNING: This argument has no effect. Do not use.) vlim : Tuple[float, float] = None The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + (WARNING: This argument has no effect. Do not use.) alpha : float = None 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 ascending axis order.) Returns ------- matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ - # background can't have source, so no need to add background structure structures = self.scene.sorted_structures @@ -1597,12 +1636,21 @@ 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) for source, shape in source_shapes: if source is not None: + if hlim or vlim: # Fix this eventually? For now, just warn users. + log.warning( + "The `hlim` and `vlim` arguments are not implemented for this plot type. " + "Sources may be displayed at the wrong location.", + log_once=True, + ) ax = self._plot_shape_structure_source( alpha=alpha, source=source, @@ -1610,14 +1658,17 @@ def plot_sources( source_max=source_max, shape=shape, ax=ax, + # I omitted `transpose` because we took care of transposing the shape earlier. ) # 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 @@ -1682,6 +1733,7 @@ def _plot_shape_structure_source( source_max: float, ax: Ax, alpha: Optional[float] = None, + transpose: bool = False, ) -> Ax: """Plot a structure's cross section shape for a given medium, grayscale for permittivity.""" plot_params = self._get_structure_source_plot_params( @@ -1690,7 +1742,7 @@ def _plot_shape_structure_source( source_max=source_max, alpha=alpha, ) - ax = self.plot_shape(shape=shape, plot_params=plot_params, ax=ax) + ax = self.plot_shape(shape=shape, plot_params=plot_params, ax=ax, transpose=transpose) return ax @classmethod diff --git a/tidy3d/plugins/adjoint/components/simulation.py b/tidy3d/plugins/adjoint/components/simulation.py index 3fcfdf0066..e7776b1362 100644 --- a/tidy3d/plugins/adjoint/components/simulation.py +++ b/tidy3d/plugins/adjoint/components/simulation.py @@ -539,6 +539,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: """Wrapper around regular :class:`.Simulation` structure plotting.""" @@ -552,6 +553,7 @@ def plot( monitor_alpha=monitor_alpha, hlim=hlim, vlim=vlim, + transpose=transpose, **patch_kwargs, ) @@ -566,6 +568,7 @@ def plot_eps( monitor_alpha: Optional[float] = None, hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, + transpose: bool = False, ax: Ax = None, ) -> Ax: """Wrapper around regular :class:`.Simulation` permittivity plotting.""" @@ -579,6 +582,7 @@ def plot_eps( monitor_alpha=monitor_alpha, hlim=hlim, vlim=vlim, + transpose=transpose, ) def plot_structures( @@ -589,6 +593,7 @@ def plot_structures( 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. @@ -606,6 +611,8 @@ def plot_structures( 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 ascending axis order) Returns ------- @@ -620,6 +627,7 @@ def plot_structures( ax=ax, hlim=hlim, vlim=vlim, + transpose=transpose, ) def plot_structures_eps( @@ -634,6 +642,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. @@ -663,6 +672,9 @@ 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 + ascending axis order.) Returns ------- @@ -681,6 +693,7 @@ def plot_structures_eps( ax=ax, hlim=hlim, vlim=vlim, + transpose=transpose, ) def epsilon( diff --git a/tidy3d/plugins/microwave/array_factor.py b/tidy3d/plugins/microwave/array_factor.py index ca688b7ee5..94c5f38d28 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/plugins/microwave/custom_path_integrals.py b/tidy3d/plugins/microwave/custom_path_integrals.py index d1e2c89a19..295ceb32cf 100644 --- a/tidy3d/plugins/microwave/custom_path_integrals.py +++ b/tidy3d/plugins/microwave/custom_path_integrals.py @@ -12,6 +12,7 @@ from tidy3d.components.base import cached_property from tidy3d.components.geometry.base import Geometry from tidy3d.components.types import ArrayFloat2D, Ax, Axis, Bound, Coordinate, Direction +from tidy3d.components.utils import warn_untested_argument from tidy3d.components.viz import add_ax_if_none from tidy3d.constants import MICROMETER, fp_eps from tidy3d.exceptions import SetupError @@ -268,6 +269,7 @@ def plot( y: Optional[float] = None, z: Optional[float] = None, ax: Ax = None, + transpose: bool = False, **path_kwargs, ) -> Ax: """Plot path integral at single (x,y,z) coordinate. @@ -282,6 +284,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 ascending axis order) **path_kwargs Optional keyword arguments passed to the matplotlib plotting of the line. For details on accepted values, refer to @@ -292,6 +296,11 @@ def plot( matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ + if transpose: + # Please remove this warning once someone has verified that `transpose=True` works. + warn_untested_argument( + cls_name=type(self).__name__, func_name="plot", arg="transpose", val="True" + ) axis, position = Geometry.parse_xyz_kwargs(x=x, y=y, z=z) if axis != self.main_axis or not np.isclose(position, self.position, rtol=fp_eps): return ax @@ -300,6 +309,8 @@ def plot( plot_kwargs = plot_params.to_kwargs() xs = self.vertices[:, 0] ys = self.vertices[:, 1] + if transpose: + xs, ys = ys, xs ax.plot(xs, ys, markevery=[0, -1], **plot_kwargs) # Plot special end points @@ -341,6 +352,7 @@ def plot( y: Optional[float] = None, z: Optional[float] = None, ax: Ax = None, + transpose: bool = False, **path_kwargs, ) -> Ax: """Plot path integral at single (x,y,z) coordinate. @@ -355,6 +367,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 ascending axis order) **path_kwargs Optional keyword arguments passed to the matplotlib plotting of the line. For details on accepted values, refer to @@ -365,6 +379,11 @@ def plot( matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ + if transpose: + # Please remove this warning once someone has verified that `transpose=True` works. + warn_untested_argument( + cls_name=type(self).__name__, func_name="plot", arg="transpose", val="True" + ) axis, position = Geometry.parse_xyz_kwargs(x=x, y=y, z=z) if axis != self.main_axis or not np.isclose(position, self.position, rtol=fp_eps): return ax @@ -373,6 +392,8 @@ def plot( plot_kwargs = plot_params.to_kwargs() xs = self.vertices[:, 0] ys = self.vertices[:, 1] + if transpose: + xs, ys = ys, xs ax.plot(xs, ys, **plot_kwargs) # Add arrow at start of contour diff --git a/tidy3d/plugins/microwave/path_integrals.py b/tidy3d/plugins/microwave/path_integrals.py index 802f5b0d92..0e6cfd4bb9 100644 --- a/tidy3d/plugins/microwave/path_integrals.py +++ b/tidy3d/plugins/microwave/path_integrals.py @@ -21,6 +21,7 @@ from tidy3d.components.data.monitor_data import FieldData, FieldTimeData, ModeData, ModeSolverData from tidy3d.components.geometry.base import Box, Geometry from tidy3d.components.types import Ax, Axis, Coordinate2D, Direction +from tidy3d.components.utils import pop_axis_and_swap, warn_untested_argument from tidy3d.components.validators import assert_line, assert_plane from tidy3d.components.viz import add_ax_if_none from tidy3d.constants import AMP, VOLT, fp_eps @@ -59,6 +60,11 @@ def remaining_axes(self) -> tuple[Axis, Axis]: return (axes[1], axes[0]) return (axes[0], axes[1]) + def remaining_axes_ascending(self, transpose: bool = False) -> tuple[Axis, Axis]: + """Get in-plane axes, in ascending order, with support for axes swapping.""" + _, axes = pop_axis_and_swap([0, 1, 2], self.main_axis, transpose=transpose) + return (axes[0], axes[1]) + @cached_property def remaining_dims(self) -> tuple[str, str]: """Get in-plane dimensions, ordered to maintain a right-handed coordinate system.""" @@ -184,12 +190,15 @@ def main_axis(self) -> Axis: return index raise Tidy3dError("Failed to identify axis.") - def _vertices_2D(self, axis: Axis) -> tuple[Coordinate2D, Coordinate2D]: - """Returns the two vertices of this path in the plane defined by ``axis``.""" + def _vertices_2D_ascending( + self, axis: Axis, transpose: bool = False + ) -> tuple[Coordinate2D, Coordinate2D]: + """Returns the two vertices of this path in the plane defined by ``axis``. + Axis order is ascending (unless transpose=True), not right-handed.""" min = self.bounds[0] max = self.bounds[1] - _, min = Box.pop_axis(min, axis) - _, max = Box.pop_axis(max, axis) + _, min = pop_axis_and_swap(min, axis, transpose=transpose) + _, max = pop_axis_and_swap(max, axis, transpose=transpose) u = [min[0], max[0]] v = [min[1], max[1]] @@ -312,6 +321,7 @@ def plot( y: Optional[float] = None, z: Optional[float] = None, ax: Ax = None, + transpose: bool = False, **path_kwargs, ) -> Ax: """Plot path integral at single (x,y,z) coordinate. @@ -326,6 +336,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 ascending axis order) **path_kwargs Optional keyword arguments passed to the matplotlib plotting of the line. For details on accepted values, refer to @@ -336,11 +348,18 @@ def plot( matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ + if transpose: + # Please remove this warning once someone has verified that `transpose=True` works. + warn_untested_argument( + cls_name=type(self).__name__, func_name="plot", arg="transpose", val="True" + ) + axis, position = self.parse_xyz_kwargs(x=x, y=y, z=z) if axis == self.main_axis or not np.isclose(position, self.center[axis], rtol=fp_eps): return ax - (xs, ys) = self._vertices_2D(axis) + (xs, ys) = self._vertices_2D_ascending(axis, transpose=transpose) + # Plot the path plot_params = plot_params_voltage_path.include_kwargs(**path_kwargs) plot_kwargs = plot_params.to_kwargs() @@ -419,7 +438,7 @@ def main_axis(self) -> Axis: raise Tidy3dError("Failed to identify axis.") def _to_path_integrals( - self, h_horizontal=None, h_vertical=None + self, h_horizontal=None, h_vertical=None, transpose: bool = False ) -> tuple[AxisAlignedPathIntegral, ...]: """Returns four ``AxisAlignedPathIntegral`` instances, which represent a contour integral around the surface defined by ``self.size``.""" @@ -511,6 +530,7 @@ def plot( y: Optional[float] = None, z: Optional[float] = None, ax: Ax = None, + transpose: bool = False, **path_kwargs, ) -> Ax: """Plot path integral at single (x,y,z) coordinate. @@ -525,6 +545,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 ascending axis order) **path_kwargs Optional keyword arguments passed to the matplotlib plotting of the line. For details on accepted values, refer to @@ -535,6 +557,12 @@ def plot( matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ + if transpose: + # Please remove this warning once someone has verified that `transpose=True` works. + warn_untested_argument( + cls_name=type(self).__name__, func_name="plot", arg="transpose", val="True" + ) + axis, position = self.parse_xyz_kwargs(x=x, y=y, z=z) if axis != self.main_axis or not np.isclose(position, self.center[axis], rtol=fp_eps): return ax @@ -544,17 +572,17 @@ def plot( path_integrals = self._to_path_integrals() # Plot the path for path in path_integrals: - (xs, ys) = path._vertices_2D(axis) + (xs, ys) = path._vertices_2D_ascending(axis, transpose=transpose) ax.plot(xs, ys, **plot_kwargs) - (ax1, ax2) = self.remaining_axes + (ax1, ax2) = self.remaining_axes_ascending(transpose=transpose) # Add arrow to bottom path, unless right path is longer arrow_path = path_integrals[0] if self.size[ax2] > self.size[ax1]: arrow_path = path_integrals[1] - (xs, ys) = arrow_path._vertices_2D(axis) + (xs, ys) = arrow_path._vertices_2D_ascending(axis, transpose=transpose) X = (xs[0] + xs[1]) / 2 Y = (ys[0] + ys[1]) / 2 center = np.array([X, Y]) diff --git a/tidy3d/plugins/smatrix/component_modelers/modal.py b/tidy3d/plugins/smatrix/component_modelers/modal.py index 251d48252a..2f15d4a6b9 100644 --- a/tidy3d/plugins/smatrix/component_modelers/modal.py +++ b/tidy3d/plugins/smatrix/component_modelers/modal.py @@ -217,6 +217,7 @@ def plot_sim( y: Optional[float] = None, z: Optional[float] = None, ax: Ax = None, + transpose: bool = False, ) -> Ax: """Plot a :class:`.Simulation` with all sources added for each port, for troubleshooting.""" @@ -225,7 +226,7 @@ def plot_sim( mode_source_0 = self.to_source(port=port_source, mode_index=0) plot_sources.append(mode_source_0) sim_plot = self.simulation.copy(update={"sources": plot_sources}) - return sim_plot.plot(x=x, y=y, z=z, ax=ax) + return sim_plot.plot(x=x, y=y, z=z, ax=ax, transpose=transpose) @equal_aspect @add_ax_if_none @@ -235,6 +236,7 @@ def plot_sim_eps( y: Optional[float] = None, z: Optional[float] = None, ax: Ax = None, + transpose: bool = False, **kwargs, ) -> Ax: """Plot permittivity of the :class:`.Simulation` with all sources added for each port.""" @@ -244,7 +246,7 @@ def plot_sim_eps( mode_source_0 = self.to_source(port=port_source, mode_index=0) plot_sources.append(mode_source_0) sim_plot = self.simulation.copy(update={"sources": plot_sources}) - return sim_plot.plot_eps(x=x, y=y, z=z, ax=ax, **kwargs) + return sim_plot.plot_eps(x=x, y=y, z=z, ax=ax, transpose=transpose, **kwargs) def _normalization_factor(self, port_source: Port, sim_data: SimulationData) -> complex: """Compute the normalization amplitude based on the measured input mode amplitude.""" diff --git a/tidy3d/plugins/smatrix/component_modelers/terminal.py b/tidy3d/plugins/smatrix/component_modelers/terminal.py index 0e5b9cad0a..24bbccc81e 100644 --- a/tidy3d/plugins/smatrix/component_modelers/terminal.py +++ b/tidy3d/plugins/smatrix/component_modelers/terminal.py @@ -64,6 +64,7 @@ def plot_sim( x: Optional[float] = None, y: Optional[float] = None, z: Optional[float] = None, + transpose: bool = False, ax: Ax = None, **kwargs, ) -> Ax: @@ -74,7 +75,7 @@ def plot_sim( source_0 = port_source.to_source(self._source_time) plot_sources.append(source_0) sim_plot = self.simulation.copy(update={"sources": plot_sources}) - return sim_plot.plot(x=x, y=y, z=z, ax=ax, **kwargs) + return sim_plot.plot(x=x, y=y, z=z, ax=ax, transpose=transpose, **kwargs) @equal_aspect @add_ax_if_none @@ -84,6 +85,7 @@ def plot_sim_eps( y: Optional[float] = None, z: Optional[float] = None, ax: Ax = None, + transpose: bool = False, **kwargs, ) -> Ax: """Plot permittivity of the :class:`.Simulation` with all sources added for each port.""" @@ -93,7 +95,7 @@ def plot_sim_eps( source_0 = port_source.to_source(self._source_time) plot_sources.append(source_0) sim_plot = self.simulation.copy(update={"sources": plot_sources}) - return sim_plot.plot_eps(x=x, y=y, z=z, ax=ax, **kwargs) + return sim_plot.plot_eps(x=x, y=y, z=z, ax=ax, transpose=transpose, **kwargs) @cached_property def sim_dict(self) -> dict[str, Simulation]: diff --git a/tidy3d/plugins/waveguide/rectangular_dielectric.py b/tidy3d/plugins/waveguide/rectangular_dielectric.py index 757f07bfd4..d50de070f1 100644 --- a/tidy3d/plugins/waveguide/rectangular_dielectric.py +++ b/tidy3d/plugins/waveguide/rectangular_dielectric.py @@ -21,6 +21,7 @@ from tidy3d.components.source.time import GaussianPulse from tidy3d.components.structure import Structure from tidy3d.components.types import TYPE_TAG_STR, ArrayFloat1D, Ax, Axis, Coordinate, Size1D +from tidy3d.components.utils import warn_untested_argument from tidy3d.components.viz import add_ax_if_none from tidy3d.constants import C_0, MICROMETER, RADIAN, inf from tidy3d.exceptions import Tidy3dError, ValidationError @@ -822,6 +823,7 @@ def plot( ax: Ax = None, source_alpha: Optional[float] = None, monitor_alpha: Optional[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. @@ -840,6 +842,8 @@ def plot( Opacity of the monitors. 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 ascending axis order) Returns ------- @@ -853,6 +857,7 @@ def plot( ax=ax, source_alpha=source_alpha, monitor_alpha=monitor_alpha, + transpose=transpose, **patch_kwargs, ) @@ -865,6 +870,7 @@ def plot_eps( alpha: Optional[float] = None, source_alpha: Optional[float] = None, monitor_alpha: Optional[float] = None, + transpose: bool = False, ax: Ax = None, ) -> Ax: """Plot each of simulation's components on a plane defined by one nonzero x,y,z coordinate. @@ -890,6 +896,8 @@ def plot_eps( Opacity of the monitors. 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 ascending axis order) Returns ------- @@ -905,6 +913,7 @@ def plot_eps( source_alpha=source_alpha, monitor_alpha=monitor_alpha, ax=ax, + transpose=transpose, ) def plot_structures( @@ -913,6 +922,7 @@ def plot_structures( y: Optional[float] = None, z: Optional[float] = None, ax: Ax = None, + transpose: bool = False, ) -> Ax: """Plot each of simulation's structures on a plane defined by one nonzero x,y,z coordinate. @@ -926,6 +936,8 @@ def plot_structures( 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 ascending axis order) Returns ------- @@ -937,6 +949,7 @@ def plot_structures( y=y, z=z, ax=ax, + transpose=transpose, ) def plot_structures_eps( @@ -949,6 +962,7 @@ def plot_structures_eps( cbar: bool = True, reverse: bool = False, ax: Ax = 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. @@ -974,6 +988,8 @@ def plot_structures_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 ascending axis order) Returns ------- @@ -989,6 +1005,7 @@ def plot_structures_eps( cbar=cbar, reverse=reverse, ax=ax, + transpose=transpose, ) def plot_grid( @@ -997,6 +1014,7 @@ def plot_grid( y: Optional[float] = None, z: Optional[float] = None, ax: Ax = None, + transpose: bool = False, **kwargs, ) -> Ax: """Plot the cell boundaries as lines on a plane defined by one nonzero x,y,z coordinate. @@ -1011,6 +1029,8 @@ def plot_grid( 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 ascending axis order) **kwargs Optional keyword arguments passed to the matplotlib ``LineCollection``. For details on accepted values, refer to @@ -1026,6 +1046,7 @@ def plot_grid( y=y, z=z, ax=ax, + transpose=transpose, **kwargs, ) @@ -1034,6 +1055,7 @@ def plot_geometry_edges( self, color: str = "k", ax: Ax = None, + transpose: bool = False, ) -> Ax: """Plot the waveguide cross-section geometry edges. @@ -1042,12 +1064,20 @@ def plot_geometry_edges( color : Color to use for the geometry edges. 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 ascending axis order) Returns ------- matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ + if transpose: + # Please remove this warning once someone has verified that `transpose=True` works. + warn_untested_argument( + cls_name=type(self).__name__, func_name="plot", arg="transpose", val="True" + ) + kwargs = {"color": color, "linewidth": pyplot.rcParams["grid.linewidth"]} if self.normal_axis < self.lateral_axis: x0 = self.origin[self.lateral_axis] @@ -1085,6 +1115,8 @@ def plot_geometry_edges( p = self._translate(x, y, 0) plot_x.append(p[u]) plot_y.append(p[v]) + if transpose: + plot_x, plot_y = plot_y, plot_x ax.plot(plot_x, plot_y, linestyle="-", **kwargs) ax.set_aspect("equal") return ax @@ -1099,6 +1131,7 @@ def plot_field( vmax: Optional[float] = None, ax: Ax = None, geometry_edges: Optional[str] = None, + transpose: bool = False, **sel_kwargs, ) -> Ax: """Plot the field for a :class:`.ModeSolverData` with :class:`.Simulation` plot overlaid. @@ -1127,6 +1160,8 @@ def plot_field( ax : matplotlib.axes._subplots.Axes = None matplotlib axes to plot on, if not specified, one is created. geometry_edges : Optional color to use for the geometry edges overlaid on the fields. + transpose : bool = False + Swap horizontal and vertical axes. (This overrides the default ascending axis order) sel_kwargs : keyword arguments used to perform ``.sel()`` selection in the monitor data. These kwargs can select over the spatial dimensions (``x``, ``y``, ``z``), frequency or time dimensions (``f``, ``t``) or `mode_index`, if applicable. @@ -1147,8 +1182,9 @@ def plot_field( vmin=vmin, vmax=vmax, ax=ax, + transpose=transpose, **sel_kwargs, ) if geometry_edges is not None: - self.plot_geometry_edges(geometry_edges, ax=ax) + self.plot_geometry_edges(geometry_edges, ax=ax, transpose=transpose) return ax