diff --git a/CHANGELOG.md b/CHANGELOG.md index 36e93d7ec6..61d487171e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bug in `TerminalComponentModelerData.get_antenna_metrics_data()` where `WavePort` mode indices were not properly handled. Improved docstrings and type hints to make the usage clearer. - Improved type hints for `Tidy3dBaseModel`, so that all derived classes will have more accurate return types. - More robust method for suppressing RF license warnings during tests. +- Fixed frequency sampling of `TransmissionLineDataset` within `MicrowaveModeData` when using group index calculation. ### Removed - Removed deprecated `use_complex_fields` parameter from `TwoPhotonAbsorption` and `KerrNonlinearity`. Parameters `beta` and `n2` are now real-valued only, as is `n0` if specified. diff --git a/tests/test_components/test_microwave.py b/tests/test_components/test_microwave.py index ee75df41ef..22ca41f79b 100644 --- a/tests/test_components/test_microwave.py +++ b/tests/test_components/test_microwave.py @@ -1156,6 +1156,88 @@ def test_mode_solver_with_microwave_mode_spec(): ) +def test_mode_solver_with_microwave_group_index(): + """Test that group_index calculation with MicrowaveModeSpec correctly filters frequencies.""" + + width = 1.0 * mm + height = 0.5 * mm + metal_thickness = 0.1 * mm + + stripline_sim = make_mw_sim( + transmission_line_type="stripline", + width=width, + height=height, + metal_thickness=metal_thickness, + ) + dl = 0.05 * mm + stripline_sim = stripline_sim.updated_copy(grid_spec=td.GridSpec.uniform(dl=dl)) + + plane = td.Box(center=(0, 0, 0), size=(0, 10 * width, 2 * height + metal_thickness)) + num_modes = 1 + + # Define original frequencies that we want in the final result + original_freqs = [1e9, 5e9, 10e9] + + # Create custom impedance spec (AutoImpedanceSpec won't work with local mode solver) + custom_spec = td.CustomImpedanceSpec( + voltage_spec=None, + current_spec=td.AxisAlignedCurrentIntegralSpec( + size=(0, width + dl, metal_thickness + dl), sign="+" + ), + ) + + # Enable group_index calculation + mode_spec = td.MicrowaveModeSpec( + num_modes=num_modes, + target_neff=2.2, + impedance_specs=custom_spec, + group_index_step=True, # This will expand frequencies to triplets + ) + + mms = ModeSolver( + simulation=stripline_sim, + plane=plane, + mode_spec=mode_spec, + colocate=False, + freqs=original_freqs, + ) + + # Get the mode solver data + mms_data: td.MicrowaveModeSolverData = mms.data + + # Verify that group index was calculated + assert mms_data.n_group is not None, "Group index should be calculated" + + # Verify that transmission line data exists + assert mms_data.transmission_line_data is not None, "Transmission line data should exist" + + # Verify that the frequencies in transmission_line_data match the original frequencies + # (not the expanded triplet used internally for group index calculation) + tl_freqs_Z0 = mms_data.transmission_line_data.Z0.coords["f"].values + tl_freqs_voltage = mms_data.transmission_line_data.voltage_coeffs.coords["f"].values + tl_freqs_current = mms_data.transmission_line_data.current_coeffs.coords["f"].values + + assert len(tl_freqs_Z0) == len(original_freqs), ( + f"Z0 should have {len(original_freqs)} frequencies, got {len(tl_freqs_Z0)}" + ) + assert len(tl_freqs_voltage) == len(original_freqs), ( + f"voltage_coeffs should have {len(original_freqs)} frequencies, got {len(tl_freqs_voltage)}" + ) + assert len(tl_freqs_current) == len(original_freqs), ( + f"current_coeffs should have {len(original_freqs)} frequencies, got {len(tl_freqs_current)}" + ) + + assert np.allclose(tl_freqs_Z0, original_freqs), ( + f"Z0 frequencies {tl_freqs_Z0} should match original {original_freqs}" + ) + assert np.allclose(tl_freqs_voltage, original_freqs), ( + f"voltage_coeffs frequencies {tl_freqs_voltage} should match original {original_freqs}" + ) + assert np.allclose(tl_freqs_current, original_freqs), ( + f"current_coeffs frequencies {tl_freqs_current} should match original {original_freqs}" + ) + + @pytest.mark.parametrize("axis", [0, 1, 2]) def test_voltage_integral_axes(axis): """Check AxisAlignedVoltageIntegral runs.""" diff --git a/tidy3d/components/data/monitor_data.py b/tidy3d/components/data/monitor_data.py index e3c6f20b88..081e8ce5c6 100644 --- a/tidy3d/components/data/monitor_data.py +++ b/tidy3d/components/data/monitor_data.py @@ -1890,6 +1890,24 @@ def _find_closest_pairs(arr: Numpy) -> tuple[Numpy, Numpy]: return pairs, values + def _group_index_freq_slices(self) -> tuple[slice, slice, slice]: + """Get frequency slices for group index numerical differentiation. + + Group index calculation uses three-point finite differences, requiring + backward, center, and forward frequency points organized as triplets. + + Returns + ------- + tuple[slice, slice, slice] + Slices for (backward, center, forward) frequencies from the frequency array. + """ + freqs = self.n_complex.coords["f"].values + num_freqs = freqs.size + back = slice(0, num_freqs, 3) + center = slice(1, num_freqs, 3) + fwd = slice(2, num_freqs, 3) + return back, center, fwd + def _group_index_post_process(self, frequency_step: float) -> ModeData: """Calculate group index and remove added frequencies used only for this calculation. @@ -1904,12 +1922,8 @@ def _group_index_post_process(self, frequency_step: float) -> ModeData: Filtered data with calculated group index. """ - freqs = self.n_complex.coords["f"].values - num_freqs = freqs.size - back = slice(0, num_freqs, 3) - center = slice(1, num_freqs, 3) - fwd = slice(2, num_freqs, 3) - freqs = freqs[center] + back, center, fwd = self._group_index_freq_slices() + freqs = self.n_complex.coords["f"].values[center] # calculate group index n_center = self.n_eff.isel(f=center).values diff --git a/tidy3d/components/microwave/data/monitor_data.py b/tidy3d/components/microwave/data/monitor_data.py index 0a31435689..64051ca5fb 100644 --- a/tidy3d/components/microwave/data/monitor_data.py +++ b/tidy3d/components/microwave/data/monitor_data.py @@ -281,6 +281,30 @@ def modes_info(self) -> xr.Dataset: super_info["Im(Z0)"] = self.transmission_line_data.Z0.imag return super_info + def _group_index_post_process(self, frequency_step: float) -> ModeData: + """Calculate group index and remove added frequencies used only for this calculation. + + Parameters + ---------- + frequency_step: float + Fractional frequency step used to calculate the group index. + + Returns + ------- + :class:`.ModeData` + Filtered data with calculated group index. + """ + super_data = super()._group_index_post_process(frequency_step) + if self.transmission_line_data is not None: + _, center_inds, _ = self._group_index_freq_slices() + update_dict = { + "Z0": self.transmission_line_data.Z0.isel(f=center_inds), + "voltage_coeffs": self.transmission_line_data.voltage_coeffs.isel(f=center_inds), + "current_coeffs": self.transmission_line_data.current_coeffs.isel(f=center_inds), + } + super_data = super_data.updated_copy(**update_dict, path="transmission_line_data") + return super_data + class MicrowaveModeSolverData(ModeSolverData, MicrowaveModeData): """