diff --git a/CHANGELOG.md b/CHANGELOG.md index 394c3dbc00..d259468bf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Polygon vertices cleanup in `ClipOperation.intersections_plane`. - Removed sources from `sim_inf_structure` simulation object in `postprocess_adj` to avoid source and background medium validation errors. - Revert overly restrictive validation of `freqs` in the `ComponentModeler` and `TerminalComponentModeler`. +- Fixed `ElectromagneticFieldData.to_zbf()` to support single frequency monitors and apply the correct flattening order. ## [2.9.0] - 2025-08-04 diff --git a/tests/test_data/test_monitor_data.py b/tests/test_data/test_monitor_data.py index c0f70d6454..7acb0b6516 100644 --- a/tests/test_data/test_monitor_data.py +++ b/tests/test_data/test_monitor_data.py @@ -906,6 +906,17 @@ def field_data(self) -> td.FieldData: ) return self.simdata(monitor)["fields"] + @pytest.fixture(scope="class") + def field_data_single_frequency(self) -> td.FieldData: + """Make random field data with single frequency from an emulated simulation run.""" + monitor = td.FieldMonitor( + size=(td.inf, td.inf, 0), + freqs=self.freqs[0], + name="fields", + colocate=True, + ) + return self.simdata(monitor)["fields"] + @pytest.fixture(scope="class") def mode_data(self) -> td.ModeData: """Make random ModeData from an emulated simulation run.""" @@ -919,18 +930,41 @@ def mode_data(self) -> td.ModeData: ) return self.simdata(monitor)["modes"] + @pytest.fixture(scope="class") + def mode_data_single_frequency(self) -> td.ModeData: + """Make random ModeData from an emulated simulation run.""" + monitor = td.ModeMonitor( + size=(td.inf, td.inf, 0), + freqs=self.freqs[0], + name="modes", + colocate=True, + mode_spec=td.ModeSpec(num_modes=2, target_neff=4.0), + store_fields_direction="+", + ) + return self.simdata(monitor)["modes"] + + @pytest.mark.parametrize("field_data_fixture", ["field_data", "field_data_single_frequency"]) @pytest.mark.parametrize("background_index", [1, 2, 3]) @pytest.mark.parametrize("freq", [*list(freqs), None]) @pytest.mark.parametrize("n_x", [2**5, 2**6]) @pytest.mark.parametrize("n_y", [2**5, 2**6]) @pytest.mark.parametrize("units", ["mm", "cm", "in", "m"]) def test_fielddata_tozbf_readzbf( - self, tmp_path, field_data, background_index, freq, n_x, n_y, units + self, + tmp_path, + request, + field_data_fixture, + background_index, + freq, + n_x, + n_y, + units, ): """Test that FieldData.to_zbf() -> ZBFData.read_zbf() works""" zbf_filename = tmp_path / "testzbf.zbf" # write to zbf and then load it back in + field_data = request.getfixturevalue(field_data_fixture) ex, ey = field_data.to_zbf( fname=zbf_filename, background_refractive_index=background_index, @@ -960,13 +994,20 @@ def test_fielddata_tozbf_readzbf( assert np.allclose(ex.values, zbfdata.Ex) assert np.allclose(ey.values, zbfdata.Ey) + @pytest.mark.parametrize("mode_data_fixture", ["mode_data", "mode_data_single_frequency"]) @pytest.mark.parametrize("mode_index", [0, 1]) - def test_tozbf_modedata(self, tmp_path, mode_data, mode_index): + def test_tozbf_modedata( + self, + tmp_path, + request, + mode_data_fixture, + mode_index, + ): """Tests ModeData.to_zbf()""" zbf_filename = tmp_path / "testzbf_modedata.zbf" # write to zbf and then load it back in - ex, ey = mode_data.to_zbf( + ex, ey = request.getfixturevalue(mode_data_fixture).to_zbf( fname=zbf_filename, background_refractive_index=1, freq=self.freq0, diff --git a/tidy3d/components/data/monitor_data.py b/tidy3d/components/data/monitor_data.py index ce79a55336..638b70b682 100644 --- a/tidy3d/components/data/monitor_data.py +++ b/tidy3d/components/data/monitor_data.py @@ -1163,9 +1163,14 @@ def to_zbf( else: freq = freq.item() - mode_area = mode_area.interp(f=freq) - e_x = e_x.interp(f=freq) - e_y = e_y.interp(f=freq) + # If the data has just one frequency, avoid Nans at the interpolation + if len(e_x.f) > 1: + mode_area = mode_area.interp(f=freq) + e_x = e_x.interp(f=freq) + e_y = e_y.interp(f=freq) + else: + e_x = e_x.isel(f=0, drop=True) + e_y = e_y.isel(f=0, drop=True) # If the data is ModeData, choose one of the modes to save if "mode_index" in e_x.coords: @@ -1241,7 +1246,7 @@ def to_zbf( ) fout.write(struct.pack("<8d", 0, 0, 0, 0, 0, 0, 0, 0)) # unused values for e in (e_x, e_y): - e_flat = e.values.flatten(order="C") + e_flat = e.values.flatten(order="F") # Interweave real and imaginary parts e_values = np.ravel(np.column_stack((e_flat.real, e_flat.imag))) fout.write(struct.pack(f"<{2 * n_x * n_y}d", *e_values)) diff --git a/tidy3d/components/data/zbf.py b/tidy3d/components/data/zbf.py index d05d058d25..08d9870216 100644 --- a/tidy3d/components/data/zbf.py +++ b/tidy3d/components/data/zbf.py @@ -121,11 +121,11 @@ def read_zbf(filename: str) -> ZBFData: ) from None # load E field - Ex_real = np.asarray(rawx[0::2]).reshape(nx, ny) - Ex_imag = np.asarray(rawx[1::2]).reshape(nx, ny) + Ex_real = np.asarray(rawx[0::2]).reshape(nx, ny, order="F") + Ex_imag = np.asarray(rawx[1::2]).reshape(nx, ny, order="F") if ispol: - Ey_real = np.asarray(rawy[0::2]).reshape(nx, ny) - Ey_imag = np.asarray(rawy[1::2]).reshape(nx, ny) + Ey_real = np.asarray(rawy[0::2]).reshape(nx, ny, order="F") + Ey_imag = np.asarray(rawy[1::2]).reshape(nx, ny, order="F") else: Ey_real = np.zeros((nx, ny)) Ey_imag = np.zeros((nx, ny))