Skip to content

Commit ceccb9e

Browse files
committed
add a MicrowaveModeSpec for RF specific mode information
add a ImpedanceSpec for controlling how characteristic impedance is calculated from modes
1 parent ab972ac commit ceccb9e

28 files changed

+2588
-763
lines changed

tests/test_components/test_microwave.py

Lines changed: 633 additions & 3 deletions
Large diffs are not rendered by default.

tests/test_components/test_mode.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,3 +386,12 @@ def test_plane_crosses_symmetry_plane_warning(monkeypatch):
386386
mode_spec=td.ModeSpec(),
387387
freqs=[td.C_0],
388388
)
389+
390+
391+
def test_mode_spec_with_microwave_mode_spec():
392+
"""Test that the number of impedance specs is validated against the number of modes."""
393+
394+
impedance_spec = (td.AutoImpedanceSpec(),)
395+
mw_mode_spec = td.MicrowaveModeSpec(impedance_spec=impedance_spec)
396+
with pytest.raises(pydantic.ValidationError):
397+
td.ModeSpec(num_modes=2, microwave_mode_spec=mw_mode_spec)

tests/test_data/test_data_arrays.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,16 @@ def test_abs():
319319
_ = data.abs
320320

321321

322+
def test_angle():
323+
# Make sure works on real data and the type is correct
324+
data = make_scalar_field_time_data_array("Ex")
325+
angle_data = data.angle
326+
assert type(data) is type(angle_data)
327+
data = make_mode_amps_data_array()
328+
angle_data = data.angle
329+
assert type(data) is type(angle_data)
330+
331+
322332
def test_heat_data_array():
323333
T = [0, 1e-12, 2e-12]
324334
_ = td.HeatDataArray((1 + 1j) * np.random.random((3,)), coords={"T": T})

tests/test_plugins/test_microwave.py

Lines changed: 275 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,21 @@
1212
from skrf.media import MLine
1313

1414
import tidy3d as td
15+
import tidy3d.components.microwave.path_integrals.current_spec
1516
import tidy3d.plugins.microwave as mw
1617
from tidy3d import FieldData
18+
from tidy3d.components.data.data_array import FreqModeDataArray
1719
from tidy3d.constants import ETA_0
1820
from tidy3d.exceptions import DataError
1921

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")
2130

2231
# Using similar code as "test_data/test_data_arrays.py"
2332
MON_SIZE = (2, 1, 0)
@@ -527,6 +536,45 @@ def test_custom_current_integral_normal_y():
527536
current_integral.compute_current(SIM_Z_DATA["field"])
528537

529538

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+
530578
def test_custom_path_integral_accuracy():
531579
"""Test the accuracy of the custom path integral."""
532580
field_data = make_coax_field_data()
@@ -572,58 +620,14 @@ def impedance_of_coaxial_cable(r1, r2, wave_impedance=td.ETA_0):
572620
assert np.allclose(Z_calc, Z_analytic, rtol=0.04)
573621

574622

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-
622623
def test_creation_from_terminal_positions():
623624
"""Test creating an VoltageIntegralAxisAligned using terminal positions."""
624625
_ = mw.VoltageIntegralAxisAligned.from_terminal_positions(
625626
plus_terminal=2, minus_terminal=1, y=2.2, z=1
626627
)
628+
_ = mw.VoltageIntegralAxisAligned.from_terminal_positions(
629+
plus_terminal=1, minus_terminal=2, y=2.2, z=1
630+
)
627631

628632

629633
def test_auto_path_integrals_for_lumped_element():
@@ -785,12 +789,232 @@ def test_lobe_measurements(apply_cyclic_extension, include_endpoint):
785789
@pytest.mark.parametrize("min_value", [0.0, 1.0])
786790
def test_lobe_plots(min_value):
787791
"""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")
791792
theta = np.linspace(0, 2 * np.pi, 301)
792793
Urad = np.cos(theta) ** 2 * np.cos(3 * theta) ** 2 + min_value
793794
lobe_measurer = mw.LobeMeasurer(angle=theta, radiation_pattern=Urad)
794795
_, ax = plt.subplots(1, 1, subplot_kw={"projection": "polar"})
795796
ax.plot(theta, Urad, "k")
796797
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

Comments
 (0)