|
12 | 12 | from skrf.media import MLine
|
13 | 13 |
|
14 | 14 | import tidy3d as td
|
| 15 | +import tidy3d.components.microwave.path_integrals.current_spec |
15 | 16 | import tidy3d.plugins.microwave as mw
|
16 | 17 | from tidy3d import FieldData
|
| 18 | +from tidy3d.components.data.data_array import FreqModeDataArray |
17 | 19 | from tidy3d.constants import ETA_0
|
18 | 20 | from tidy3d.exceptions import DataError
|
19 | 21 |
|
20 |
| -from ..utils import get_spatial_coords_dict, run_emulated |
| 22 | +from ..utils import AssertLogLevel, get_spatial_coords_dict, run_emulated |
| 23 | + |
| 24 | +MAKE_PLOTS = False |
| 25 | +if MAKE_PLOTS: |
| 26 | + # Interative plotting for debugging |
| 27 | + from matplotlib import use |
| 28 | + |
| 29 | + use("TkAgg") |
21 | 30 |
|
22 | 31 | # Using similar code as "test_data/test_data_arrays.py"
|
23 | 32 | MON_SIZE = (2, 1, 0)
|
@@ -527,6 +536,45 @@ def test_custom_current_integral_normal_y():
|
527 | 536 | current_integral.compute_current(SIM_Z_DATA["field"])
|
528 | 537 |
|
529 | 538 |
|
| 539 | +def test_composite_current_integral_warnings(): |
| 540 | + """Ensures that the checks function correctly on some test data.""" |
| 541 | + f = [2e9, 3e9, 4e9] |
| 542 | + mode_index = list(np.arange(5)) |
| 543 | + coords = {"f": f, "mode_index": mode_index} |
| 544 | + values = np.ones((3, 5)) |
| 545 | + |
| 546 | + path_spec = ( |
| 547 | + tidy3d.components.microwave.path_integrals.current_spec.CurrentIntegralAxisAlignedSpec( |
| 548 | + center=(0, 0, 0), size=(2, 2, 0), sign="+" |
| 549 | + ) |
| 550 | + ) |
| 551 | + composite_integral = mw.CompositeCurrentIntegral( |
| 552 | + center=(0, 0, 0), size=(4, 4, 0), path_specs=[path_spec], sum_spec="split" |
| 553 | + ) |
| 554 | + |
| 555 | + phase_diff = FreqModeDataArray(np.angle(values), coords=coords) |
| 556 | + with AssertLogLevel(None): |
| 557 | + assert composite_integral._check_phase_sign_consistency(phase_diff) |
| 558 | + |
| 559 | + values[1, 2:] = -1 |
| 560 | + phase_diff = FreqModeDataArray(np.angle(values), coords=coords) |
| 561 | + with AssertLogLevel("WARNING"): |
| 562 | + assert not composite_integral._check_phase_sign_consistency(phase_diff) |
| 563 | + |
| 564 | + values = np.ones((3, 5)) |
| 565 | + in_phase = FreqModeDataArray(values, coords=coords) |
| 566 | + values = 0.5 * np.ones((3, 5)) |
| 567 | + out_phase = FreqModeDataArray(values, coords=coords) |
| 568 | + with AssertLogLevel(None): |
| 569 | + assert composite_integral._check_phase_amplitude_consistency(in_phase, out_phase) |
| 570 | + |
| 571 | + values = 0.5 * np.ones((3, 5)) |
| 572 | + values[2, 4:] = 1.5 |
| 573 | + out_phase = FreqModeDataArray(values, coords=coords) |
| 574 | + with AssertLogLevel("WARNING"): |
| 575 | + assert not composite_integral._check_phase_amplitude_consistency(in_phase, out_phase) |
| 576 | + |
| 577 | + |
530 | 578 | def test_custom_path_integral_accuracy():
|
531 | 579 | """Test the accuracy of the custom path integral."""
|
532 | 580 | field_data = make_coax_field_data()
|
@@ -572,58 +620,14 @@ def impedance_of_coaxial_cable(r1, r2, wave_impedance=td.ETA_0):
|
572 | 620 | assert np.allclose(Z_calc, Z_analytic, rtol=0.04)
|
573 | 621 |
|
574 | 622 |
|
575 |
| -def test_path_integral_plotting(): |
576 |
| - """Test that all types of path integrals correctly plot themselves.""" |
577 |
| - |
578 |
| - mean_radius = (COAX_R2 + COAX_R1) * 0.5 |
579 |
| - size = [COAX_R2 - COAX_R1, 0, 0] |
580 |
| - center = [mean_radius, 0, 0] |
581 |
| - |
582 |
| - voltage_integral = mw.VoltageIntegralAxisAligned( |
583 |
| - center=center, size=size, sign="-", extrapolate_to_endpoints=True, snap_path_to_grid=True |
584 |
| - ) |
585 |
| - |
586 |
| - current_integral = mw.CustomCurrentIntegral2D.from_circular_path( |
587 |
| - center=(0, 0, 0), radius=0.4, num_points=31, normal_axis=2, clockwise=False |
588 |
| - ) |
589 |
| - |
590 |
| - ax = voltage_integral.plot(z=0) |
591 |
| - current_integral.plot(z=0, ax=ax) |
592 |
| - plt.close() |
593 |
| - |
594 |
| - # Test off center plotting |
595 |
| - ax = voltage_integral.plot(z=2) |
596 |
| - current_integral.plot(z=2, ax=ax) |
597 |
| - plt.close() |
598 |
| - |
599 |
| - # Plot |
600 |
| - voltage_integral = mw.CustomVoltageIntegral2D( |
601 |
| - axis=1, position=0, vertices=[(-1, -1), (0, 0), (1, 1)] |
602 |
| - ) |
603 |
| - |
604 |
| - current_integral = mw.CurrentIntegralAxisAligned( |
605 |
| - center=(0, 0, 0), |
606 |
| - size=(2, 0, 1), |
607 |
| - sign="-", |
608 |
| - extrapolate_to_endpoints=False, |
609 |
| - snap_contour_to_grid=False, |
610 |
| - ) |
611 |
| - |
612 |
| - ax = voltage_integral.plot(y=0) |
613 |
| - current_integral.plot(y=0, ax=ax) |
614 |
| - plt.close() |
615 |
| - |
616 |
| - # Test off center plotting |
617 |
| - ax = voltage_integral.plot(y=2) |
618 |
| - current_integral.plot(y=2, ax=ax) |
619 |
| - plt.close() |
620 |
| - |
621 |
| - |
622 | 623 | def test_creation_from_terminal_positions():
|
623 | 624 | """Test creating an VoltageIntegralAxisAligned using terminal positions."""
|
624 | 625 | _ = mw.VoltageIntegralAxisAligned.from_terminal_positions(
|
625 | 626 | plus_terminal=2, minus_terminal=1, y=2.2, z=1
|
626 | 627 | )
|
| 628 | + _ = mw.VoltageIntegralAxisAligned.from_terminal_positions( |
| 629 | + plus_terminal=1, minus_terminal=2, y=2.2, z=1 |
| 630 | + ) |
627 | 631 |
|
628 | 632 |
|
629 | 633 | def test_auto_path_integrals_for_lumped_element():
|
@@ -785,12 +789,232 @@ def test_lobe_measurements(apply_cyclic_extension, include_endpoint):
|
785 | 789 | @pytest.mark.parametrize("min_value", [0.0, 1.0])
|
786 | 790 | def test_lobe_plots(min_value):
|
787 | 791 | """Run the lobe measurer on some test data and plot the results."""
|
788 |
| - # Interative plotting for debugging |
789 |
| - # from matplotlib import use |
790 |
| - # use("TkAgg") |
791 | 792 | theta = np.linspace(0, 2 * np.pi, 301)
|
792 | 793 | Urad = np.cos(theta) ** 2 * np.cos(3 * theta) ** 2 + min_value
|
793 | 794 | lobe_measurer = mw.LobeMeasurer(angle=theta, radiation_pattern=Urad)
|
794 | 795 | _, ax = plt.subplots(1, 1, subplot_kw={"projection": "polar"})
|
795 | 796 | ax.plot(theta, Urad, "k")
|
796 | 797 | lobe_measurer.plot(0, ax)
|
| 798 | + if MAKE_PLOTS: |
| 799 | + plt.show() |
| 800 | + |
| 801 | + |
| 802 | +def test_composite_current_integral_compute_current(): |
| 803 | + """Test CompositeCurrentIntegral.compute_current method with different sum_spec behaviors.""" |
| 804 | + |
| 805 | + # Create individual path specs for the composite |
| 806 | + path_spec1 = mw.CurrentIntegralAxisAligned(center=(0, 0, 0), size=(0.5, 0.5, 0), sign="+") |
| 807 | + path_spec2 = mw.CurrentIntegralAxisAligned(center=(0.25, 0, 0), size=(0.5, 0.5, 0), sign="-") |
| 808 | + |
| 809 | + # Test with sum_spec="sum" |
| 810 | + composite_integral_sum = mw.CompositeCurrentIntegral( |
| 811 | + center=(0, 0, 0), size=(1, 1, 0), path_specs=[path_spec1, path_spec2], sum_spec="sum" |
| 812 | + ) |
| 813 | + |
| 814 | + current_sum = composite_integral_sum.compute_current(SIM_Z_DATA["field"]) |
| 815 | + assert current_sum is not None |
| 816 | + assert hasattr(current_sum, "values") |
| 817 | + |
| 818 | + # Test with sum_spec="split" |
| 819 | + composite_integral_split = mw.CompositeCurrentIntegral( |
| 820 | + center=(0, 0, 0), size=(1, 1, 0), path_specs=[path_spec1, path_spec2], sum_spec="split" |
| 821 | + ) |
| 822 | + |
| 823 | + current_split = composite_integral_split.compute_current(SIM_Z_DATA["field"]) |
| 824 | + assert current_split is not None |
| 825 | + assert hasattr(current_split, "values") |
| 826 | + |
| 827 | + # Test that both methods return results with the same dimensions |
| 828 | + assert current_sum.dims == current_split.dims |
| 829 | + |
| 830 | + |
| 831 | +def test_composite_current_integral_time_domain_error(): |
| 832 | + """Test that CompositeCurrentIntegral raises error for time domain data with split sum_spec.""" |
| 833 | + |
| 834 | + path_spec = mw.CurrentIntegralAxisAligned(center=(0, 0, 0), size=(0.5, 0.5, 0), sign="+") |
| 835 | + |
| 836 | + composite_integral = mw.CompositeCurrentIntegral( |
| 837 | + center=(0, 0, 0), size=(1, 1, 0), path_specs=[path_spec], sum_spec="split" |
| 838 | + ) |
| 839 | + |
| 840 | + # Should raise DataError for time domain data with split sum_spec |
| 841 | + with pytest.raises( |
| 842 | + td.exceptions.DataError, match="Only frequency domain field data is supported" |
| 843 | + ): |
| 844 | + composite_integral.compute_current(SIM_Z_DATA["field_time"]) |
| 845 | + |
| 846 | + |
| 847 | +def test_composite_current_integral_phase_consistency_warnings(): |
| 848 | + """Test CompositeCurrentIntegral phase consistency warning methods.""" |
| 849 | + from tidy3d.components.data.data_array import FreqModeDataArray |
| 850 | + |
| 851 | + # Create a composite integral for testing |
| 852 | + path_spec = mw.CurrentIntegralAxisAligned(center=(0, 0, 0), size=(0.5, 0.5, 0), sign="+") |
| 853 | + |
| 854 | + composite_integral = mw.CompositeCurrentIntegral( |
| 855 | + center=(0, 0, 0), size=(1, 1, 0), path_specs=[path_spec], sum_spec="split" |
| 856 | + ) |
| 857 | + |
| 858 | + # Test _check_phase_sign_consistency with consistent data |
| 859 | + f = [2e9, 3e9, 4e9] |
| 860 | + mode_index = list(np.arange(3)) |
| 861 | + coords = {"f": f, "mode_index": mode_index} |
| 862 | + |
| 863 | + # Phase difference data that is consistent (all in phase) |
| 864 | + consistent_phase_values = np.zeros((3, 3)) # All zeros = in phase |
| 865 | + consistent_phase_diff = FreqModeDataArray(consistent_phase_values, coords=coords) |
| 866 | + |
| 867 | + # This should return True (no warning) |
| 868 | + result = composite_integral._check_phase_sign_consistency(consistent_phase_diff) |
| 869 | + assert result is True |
| 870 | + |
| 871 | + # Phase difference data that is inconsistent |
| 872 | + inconsistent_phase_values = np.array([[0, 0, 0], [0, np.pi, 0], [0, 0, np.pi]]) # Mixed phases |
| 873 | + inconsistent_phase_diff = FreqModeDataArray(inconsistent_phase_values, coords=coords) |
| 874 | + |
| 875 | + # This should return False and emit a warning |
| 876 | + # Note: The warning is logged, but we'll just test the return value here |
| 877 | + result = composite_integral._check_phase_sign_consistency(inconsistent_phase_diff) |
| 878 | + assert result is False |
| 879 | + |
| 880 | + # Test _check_phase_amplitude_consistency |
| 881 | + current_values = np.ones((3, 3)) |
| 882 | + current_in_phase = FreqModeDataArray(current_values, coords=coords) |
| 883 | + current_out_phase = FreqModeDataArray(0.5 * current_values, coords=coords) |
| 884 | + |
| 885 | + # Consistent amplitudes (in_phase always larger) |
| 886 | + result = composite_integral._check_phase_amplitude_consistency( |
| 887 | + current_in_phase, current_out_phase |
| 888 | + ) |
| 889 | + assert result is True |
| 890 | + |
| 891 | + # Inconsistent amplitudes (mix of which is larger) |
| 892 | + inconsistent_out_phase = FreqModeDataArray( |
| 893 | + np.array([[0.5, 0.5, 0.5], [1.5, 0.5, 0.5], [0.5, 1.5, 0.5]]), coords=coords |
| 894 | + ) |
| 895 | + |
| 896 | + # This should return False and emit a warning |
| 897 | + # Note: The warning is logged, but we'll just test the return value here |
| 898 | + result = composite_integral._check_phase_amplitude_consistency( |
| 899 | + current_in_phase, inconsistent_out_phase |
| 900 | + ) |
| 901 | + assert result is False |
| 902 | + |
| 903 | + |
| 904 | +def test_impedance_calculator_compute_impedance_with_return_extras(): |
| 905 | + """Test ImpedanceCalculator.compute_impedance with return_voltage_and_current=True.""" |
| 906 | + |
| 907 | + # Setup path integrals |
| 908 | + voltage_integral = mw.VoltageIntegralAxisAligned( |
| 909 | + center=(0, 0, 0), size=(0, 0.5, 0), sign="+", extrapolate_to_endpoints=True |
| 910 | + ) |
| 911 | + current_integral = mw.CurrentIntegralAxisAligned(center=(0, 0, 0), size=(0.5, 0.5, 0), sign="+") |
| 912 | + |
| 913 | + # Test with both voltage and current integrals |
| 914 | + Z_calc = mw.ImpedanceCalculator( |
| 915 | + voltage_integral=voltage_integral, current_integral=current_integral |
| 916 | + ) |
| 917 | + |
| 918 | + # Test with mode data that supports flux calculations |
| 919 | + result = Z_calc.compute_impedance(SIM_Z_DATA["mode"], return_voltage_and_current=True) |
| 920 | + |
| 921 | + # Should return a tuple of (impedance, voltage, current) |
| 922 | + assert isinstance(result, tuple) |
| 923 | + assert len(result) == 3 |
| 924 | + impedance, voltage, current = result |
| 925 | + |
| 926 | + assert impedance is not None |
| 927 | + assert voltage is not None |
| 928 | + assert current is not None |
| 929 | + assert hasattr(impedance, "values") |
| 930 | + assert hasattr(voltage, "values") |
| 931 | + assert hasattr(current, "values") |
| 932 | + |
| 933 | + # Test with only voltage integral (current computed from flux) |
| 934 | + Z_calc_voltage_only = mw.ImpedanceCalculator(voltage_integral=voltage_integral) |
| 935 | + |
| 936 | + result_voltage_only = Z_calc_voltage_only.compute_impedance( |
| 937 | + SIM_Z_DATA["mode"], return_voltage_and_current=True |
| 938 | + ) |
| 939 | + |
| 940 | + assert isinstance(result_voltage_only, tuple) |
| 941 | + assert len(result_voltage_only) == 3 |
| 942 | + impedance_v, voltage_v, current_v = result_voltage_only |
| 943 | + |
| 944 | + assert impedance_v is not None |
| 945 | + assert voltage_v is not None |
| 946 | + assert current_v is not None # Should be computed from flux |
| 947 | + |
| 948 | + # Test with only current integral (voltage computed from flux) |
| 949 | + Z_calc_current_only = mw.ImpedanceCalculator(current_integral=current_integral) |
| 950 | + |
| 951 | + result_current_only = Z_calc_current_only.compute_impedance( |
| 952 | + SIM_Z_DATA["mode"], return_voltage_and_current=True |
| 953 | + ) |
| 954 | + |
| 955 | + assert isinstance(result_current_only, tuple) |
| 956 | + assert len(result_current_only) == 3 |
| 957 | + impedance_c, voltage_c, current_c = result_current_only |
| 958 | + |
| 959 | + assert impedance_c is not None |
| 960 | + assert voltage_c is not None # Should be computed from flux |
| 961 | + assert current_c is not None |
| 962 | + |
| 963 | + |
| 964 | +def test_composite_current_integral_freq_mode_data(): |
| 965 | + """Test CompositeCurrentIntegral works correctly with FreqModeDataArray.""" |
| 966 | + |
| 967 | + # Create individual path specs for the composite |
| 968 | + path_spec1 = mw.CurrentIntegralAxisAligned(center=(0, 0, 0), size=(0.5, 0.5, 0), sign="+") |
| 969 | + path_spec2 = mw.CurrentIntegralAxisAligned(center=(0.25, 0, 0), size=(0.5, 0.5, 0), sign="-") |
| 970 | + |
| 971 | + # Test with sum_spec="sum" - should work with FreqModeDataArray |
| 972 | + composite_integral_sum = mw.CompositeCurrentIntegral( |
| 973 | + center=(0, 0, 0), size=(1, 1, 0), path_specs=[path_spec1, path_spec2], sum_spec="sum" |
| 974 | + ) |
| 975 | + |
| 976 | + # Use mode data which provides FreqModeDataArray |
| 977 | + current_sum = composite_integral_sum.compute_current(SIM_Z_DATA["mode"]) |
| 978 | + assert current_sum is not None |
| 979 | + assert hasattr(current_sum, "values") |
| 980 | + |
| 981 | + # Verify it's a FreqModeDataArray by checking dimensions |
| 982 | + assert "f" in current_sum.dims |
| 983 | + assert "mode_index" in current_sum.dims |
| 984 | + |
| 985 | + # Test with sum_spec="split" - should also work with FreqModeDataArray |
| 986 | + composite_integral_split = mw.CompositeCurrentIntegral( |
| 987 | + center=(0, 0, 0), size=(1, 1, 0), path_specs=[path_spec1, path_spec2], sum_spec="split" |
| 988 | + ) |
| 989 | + |
| 990 | + current_split = composite_integral_split.compute_current(SIM_Z_DATA["mode"]) |
| 991 | + assert current_split is not None |
| 992 | + assert hasattr(current_split, "values") |
| 993 | + |
| 994 | + # Verify it's a FreqModeDataArray by checking dimensions |
| 995 | + assert "f" in current_split.dims |
| 996 | + assert "mode_index" in current_split.dims |
| 997 | + |
| 998 | + # Test that both methods return compatible results |
| 999 | + assert current_sum.dims == current_split.dims |
| 1000 | + assert current_sum.shape == current_split.shape |
| 1001 | + |
| 1002 | + |
| 1003 | +def test_impedance_calculator_mode_direction_handling(): |
| 1004 | + """Test that ImpedanceCalculator properly handles mode direction for flux calculation.""" |
| 1005 | + |
| 1006 | + current_integral = mw.CurrentIntegralAxisAligned(center=(0, 0, 0), size=(0.5, 0.5, 0), sign="+") |
| 1007 | + |
| 1008 | + # Test with ModeSolverMonitor data |
| 1009 | + Z_calc = mw.ImpedanceCalculator(current_integral=current_integral) |
| 1010 | + |
| 1011 | + impedance_mode_solver = Z_calc.compute_impedance(SIM_Z_DATA["mode_solver"]) |
| 1012 | + assert impedance_mode_solver is not None |
| 1013 | + |
| 1014 | + # Test with ModeMonitor data |
| 1015 | + impedance_mode = Z_calc.compute_impedance(SIM_Z_DATA["mode"]) |
| 1016 | + assert impedance_mode is not None |
| 1017 | + |
| 1018 | + # Both should produce valid impedance values |
| 1019 | + assert hasattr(impedance_mode_solver, "values") |
| 1020 | + assert hasattr(impedance_mode, "values") |
0 commit comments