| 
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