Skip to content

Commit 2541396

Browse files
committed
added ability to define a wave port using only a single voltage or current path
1 parent 40bd0b2 commit 2541396

File tree

8 files changed

+127
-64
lines changed

8 files changed

+127
-64
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- `eps_component` argument in `td.Simulation.plot_eps()` to optionally select a specific permittivity component to plot (eg. `"xx"`).
1515
- Monitor `AuxFieldTimeMonitor` for aux fields like the free carrier density in `TwoPhotonAbsorption`.
1616
- Broadband handling (`num_freqs` argument) to the TFSF source.
17+
- Ability to define a `WavePort` using only a voltage or current path integral, with the missing quantity inferred via power conservation.
1718

1819
### Fixed
1920
- Compatibility with `xarray>=2025.03`.

tests/test_plugins/smatrix/terminal_component_modeler_def.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,8 @@ def make_coaxial_component_modeler(
251251
CoaxialLumpedPort,
252252
CoaxialLumpedPort,
253253
),
254+
use_current: bool = True,
255+
use_voltage: bool = True,
254256
**kwargs,
255257
):
256258
if not length:
@@ -279,27 +281,34 @@ def make_port(center, direction, type, name) -> Union[CoaxialLumpedPort, WavePor
279281
voltage_center[0] += mean_radius
280282
voltage_size = [Router - Rinner, 0, 0]
281283

282-
port = WavePort(
283-
center=center,
284-
size=[2 * Router, 2 * Router, 0],
285-
direction=direction,
286-
name=name,
287-
mode_spec=td.ModeSpec(num_modes=1),
288-
mode_index=0,
289-
voltage_integral=microwave.VoltageIntegralAxisAligned(
284+
voltage_integral = None
285+
if use_voltage:
286+
voltage_integral = microwave.VoltageIntegralAxisAligned(
290287
center=voltage_center,
291288
size=voltage_size,
292289
extrapolate_to_endpoints=True,
293290
snap_path_to_grid=True,
294291
sign="+",
295-
),
296-
current_integral=microwave.CustomCurrentIntegral2D.from_circular_path(
292+
)
293+
current_integral = None
294+
if use_current:
295+
current_integral = microwave.CustomCurrentIntegral2D.from_circular_path(
297296
center=center,
298297
radius=mean_radius,
299298
num_points=41,
300299
normal_axis=2,
301300
clockwise=direction != "+",
302-
),
301+
)
302+
303+
port = WavePort(
304+
center=center,
305+
size=[2 * Router, 2 * Router, 0],
306+
direction=direction,
307+
name=name,
308+
mode_spec=td.ModeSpec(num_modes=1),
309+
mode_index=0,
310+
voltage_integral=voltage_integral,
311+
current_integral=current_integral,
303312
)
304313
return port
305314

tests/test_plugins/smatrix/test_terminal_component_modeler.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ def test_converting_port_to_simulation_objects(snap_center):
253253
port = LumpedPort(center=(0, 0, 0), size=(0, 1, 2), voltage_axis=2, impedance=50, name="Port1")
254254
freqs = np.linspace(1e9, 10e9, 11)
255255
source_time = td.GaussianPulse(freq0=5e9, fwidth=9e9)
256-
_ = port.to_field_monitors(freqs=freqs, snap_center=snap_center)
256+
_ = port.to_monitors(freqs=freqs, snap_center=snap_center)
257257
_ = port.to_source(source_time=source_time, snap_center=snap_center)
258258

259259

@@ -413,17 +413,38 @@ def test_make_coaxial_component_modeler_with_wave_ports(tmp_path):
413413
xy_grid = td.UniformGrid(dl=0.1 * 1e3)
414414
grid_spec = td.GridSpec(grid_x=xy_grid, grid_y=xy_grid, grid_z=z_grid)
415415
_ = make_coaxial_component_modeler(
416-
path_dir=str(tmp_path), port_types=(WavePort, WavePort), grid_spec=grid_spec
416+
path_dir=str(tmp_path),
417+
port_types=(WavePort, WavePort),
418+
grid_spec=grid_spec,
417419
)
418420

419421

420-
def test_run_coaxial_component_modeler_with_wave_ports(monkeypatch, tmp_path):
422+
@pytest.mark.parametrize("voltage_enabled", [False, True])
423+
@pytest.mark.parametrize("current_enabled", [False, True])
424+
def test_run_coaxial_component_modeler_with_wave_ports(
425+
monkeypatch, tmp_path, voltage_enabled, current_enabled
426+
):
421427
"""Checks that the terminal component modeler runs with wave ports."""
422428
z_grid = td.UniformGrid(dl=1 * 1e3)
423429
xy_grid = td.UniformGrid(dl=0.1 * 1e3)
424430
grid_spec = td.GridSpec(grid_x=xy_grid, grid_y=xy_grid, grid_z=z_grid)
431+
if not (voltage_enabled or current_enabled):
432+
with pytest.raises(pd.ValidationError):
433+
modeler = make_coaxial_component_modeler(
434+
path_dir=str(tmp_path),
435+
port_types=(WavePort, WavePort),
436+
grid_spec=grid_spec,
437+
use_voltage=voltage_enabled,
438+
use_current=current_enabled,
439+
)
440+
return
441+
425442
modeler = make_coaxial_component_modeler(
426-
path_dir=str(tmp_path), port_types=(WavePort, WavePort), grid_spec=grid_spec
443+
path_dir=str(tmp_path),
444+
port_types=(WavePort, WavePort),
445+
grid_spec=grid_spec,
446+
use_voltage=voltage_enabled,
447+
use_current=current_enabled,
427448
)
428449
s_matrix = run_component_modeler(monkeypatch, modeler)
429450

tidy3d/plugins/microwave/impedance_calculator.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,15 @@ def compute_impedance(self, em_field: MonitorDataTypes) -> IntegralResultTypes:
7272
if not self.voltage_integral:
7373
flux = em_field.flux
7474
if isinstance(em_field, FieldTimeData):
75-
voltage = flux / current
75+
voltage = flux.abs / current
7676
else:
77-
voltage = 2 * flux / np.conj(current)
77+
voltage = 2 * flux.abs / np.conj(current)
7878
if not self.current_integral:
7979
flux = em_field.flux
8080
if isinstance(em_field, FieldTimeData):
81-
current = flux / voltage
81+
current = flux.abs / voltage
8282
else:
83-
current = np.conj(2 * flux / voltage)
83+
current = np.conj(2 * flux.abs / voltage)
8484

8585
impedance = voltage / current
8686
impedance = ImpedanceCalculator._set_data_array_attributes(impedance)

tidy3d/plugins/smatrix/component_modelers/terminal.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def sim_dict(self) -> Dict[str, Simulation]:
113113
field_monitors = [
114114
mon
115115
for port in self.ports
116-
for mon in port.to_field_monitors(
116+
for mon in port.to_monitors(
117117
self.freqs, snap_center=snap_centers.get(port.name), grid=sim_wo_source.grid
118118
)
119119
]
@@ -149,15 +149,13 @@ def sim_dict(self) -> Dict[str, Simulation]:
149149

150150
# Now, create simulations with wave port sources and mode solver monitors for computing port modes
151151
for wave_port in self._wave_ports:
152-
mode_monitor = wave_port.to_mode_solver_monitor(freqs=self.freqs)
153152
# Source is placed just before the field monitor of the port
154153
mode_src_pos = wave_port.center[wave_port.injection_axis] + self._shift_value_signed(
155154
wave_port
156155
)
157156
port_source = wave_port.to_source(self._source_time, snap_center=mode_src_pos)
158157

159-
new_mnts_for_wave = new_mnts + [mode_monitor]
160-
update_dict = dict(monitors=new_mnts_for_wave, sources=[port_source])
158+
update_dict = dict(sources=[port_source])
161159

162160
task_name = self._task_name(port=wave_port)
163161
sim_dict[task_name] = sim_wo_source.copy(update=update_dict)

tidy3d/plugins/smatrix/ports/base_lumped.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def to_voltage_monitor(self, freqs: FreqArray, snap_center: float = None) -> Fie
7575
def to_current_monitor(self, freqs: FreqArray, snap_center: float = None) -> FieldMonitor:
7676
"""Field monitor to compute port current."""
7777

78-
def to_field_monitors(
78+
def to_monitors(
7979
self, freqs: FreqArray, snap_center: float = None, grid: Grid = None
8080
) -> list[FieldMonitor]:
8181
"""Field monitors to compute port voltage and current."""

tidy3d/plugins/smatrix/ports/base_terminal.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
"""Class and custom data array for representing a scattering-matrix port, which is defined by a pair of terminals."""
22

33
from abc import ABC, abstractmethod
4+
from typing import Union
45

56
import pydantic.v1 as pd
67

78
from ....components.base import Tidy3dBaseModel, cached_property
89
from ....components.data.data_array import FreqDataArray
910
from ....components.data.sim_data import SimulationData
1011
from ....components.grid.grid import Grid
11-
from ....components.monitor import FieldMonitor
12+
from ....components.monitor import FieldMonitor, ModeMonitor
1213
from ....components.source.base import Source
1314
from ....components.source.time import GaussianPulse
1415
from ....components.types import FreqArray
16+
from ....log import log
1517

1618

1719
class AbstractTerminalPort(Tidy3dBaseModel, ABC):
@@ -38,11 +40,21 @@ def to_source(
3840
) -> Source:
3941
"""Create a current source from a terminal-based port."""
4042

41-
@abstractmethod
4243
def to_field_monitors(
4344
self, freqs: FreqArray, snap_center: float = None, grid: Grid = None
44-
) -> list[FieldMonitor]:
45-
"""Field monitors to compute port voltage and current."""
45+
) -> Union[list[FieldMonitor], list[ModeMonitor]]:
46+
"""DEPRECATED: Monitors used to compute the port voltage and current."""
47+
log.warning(
48+
"'to_field_monitors' method name is deprecated and will be removed in the future. Please use "
49+
"'to_monitors' for the same effect."
50+
)
51+
return self.to_monitors(freqs=freqs, snap_center=snap_center, grid=grid)
52+
53+
@abstractmethod
54+
def to_monitors(
55+
self, freqs: FreqArray, snap_center: float = None, grid: Grid = None
56+
) -> Union[list[FieldMonitor], list[ModeMonitor]]:
57+
"""Monitors used to compute the port voltage and current."""
4658

4759
@abstractmethod
4860
def compute_voltage(self, sim_data: SimulationData) -> FreqDataArray:

tidy3d/plugins/smatrix/ports/wave.py

Lines changed: 58 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
import numpy as np
66
import pydantic.v1 as pd
77

8-
from ....components.base import cached_property
8+
from ....components.base import cached_property, skip_if_fields_missing
99
from ....components.data.data_array import FreqDataArray, FreqModeDataArray
10-
from ....components.data.monitor_data import ModeSolverData
10+
from ....components.data.monitor_data import ModeData
1111
from ....components.data.sim_data import SimulationData
1212
from ....components.geometry.base import Box
1313
from ....components.grid.grid import Grid
14-
from ....components.monitor import FieldMonitor, ModeSolverMonitor
14+
from ....components.monitor import ModeMonitor
1515
from ....components.simulation import Simulation
1616
from ....components.source.field import ModeSource, ModeSpec
1717
from ....components.source.time import GaussianPulse
@@ -62,16 +62,35 @@ class WavePort(AbstractTerminalPort, Box):
6262
description="Definition of current integral used to compute current and the characteristic impedance.",
6363
)
6464

65+
def _mode_voltage_coefficients(self, mode_data: ModeData) -> FreqModeDataArray:
66+
"""Calculates scaling coefficients to convert mode amplitudes
67+
to the total port voltage.
68+
"""
69+
mode_data = mode_data._isel(mode_index=[self.mode_index])
70+
if self.voltage_integral is None:
71+
current_coeffs = self.current_integral.compute_current(mode_data)
72+
voltage_coeffs = 2 * np.abs(mode_data.flux) / np.conj(current_coeffs)
73+
else:
74+
voltage_coeffs = self.voltage_integral.compute_voltage(mode_data)
75+
return voltage_coeffs.squeeze()
76+
77+
def _mode_current_coefficients(self, mode_data: ModeData) -> FreqModeDataArray:
78+
"""Calculates scaling coefficients to convert mode amplitudes
79+
to the total port current.
80+
"""
81+
mode_data = mode_data._isel(mode_index=[self.mode_index])
82+
if self.current_integral is None:
83+
voltage_coeffs = self.voltage_integral.compute_voltage(mode_data)
84+
current_coeffs = (2 * np.abs(mode_data.flux) / voltage_coeffs).conj()
85+
else:
86+
current_coeffs = self.current_integral.compute_current(mode_data)
87+
return current_coeffs.squeeze()
88+
6589
@cached_property
6690
def injection_axis(self):
6791
"""Injection axis of the port."""
6892
return self.size.index(0.0)
6993

70-
@cached_property
71-
def _field_monitor_name(self) -> str:
72-
"""Return the name of the :class:`.FieldMonitor` associated with this port."""
73-
return f"{self.name}_field"
74-
7594
@cached_property
7695
def _mode_monitor_name(self) -> str:
7796
"""Return the name of the :class:`.ModeMonitor` associated with this port."""
@@ -92,35 +111,24 @@ def to_source(self, source_time: GaussianPulse, snap_center: float = None) -> Mo
92111
name=self.name,
93112
)
94113

95-
def to_field_monitors(
114+
def to_monitors(
96115
self, freqs: FreqArray, snap_center: float = None, grid: Grid = None
97-
) -> list[FieldMonitor]:
98-
"""Field monitor to compute port voltage and current."""
116+
) -> list[ModeMonitor]:
117+
"""The wave port uses a :class:`.ModeMonitor` to compute the characteristic impedance
118+
and the port voltages and currents."""
99119
center = list(self.center)
100120
if snap_center:
101121
center[self.injection_axis] = snap_center
102-
field_mon = FieldMonitor(
103-
center=center,
104-
size=self.size,
105-
freqs=freqs,
106-
name=self._field_monitor_name,
107-
colocate=False,
108-
)
109-
return [field_mon]
110-
111-
def to_mode_solver_monitor(self, freqs: FreqArray) -> ModeSolverMonitor:
112-
"""Mode solver monitor to compute modes that will be used to
113-
compute characteristic impedances."""
114-
mode_mon = ModeSolverMonitor(
122+
mode_mon = ModeMonitor(
115123
center=self.center,
116124
size=self.size,
117125
freqs=freqs,
118126
name=self._mode_monitor_name,
119127
colocate=False,
120128
mode_spec=self.mode_spec,
121-
direction=self.direction,
129+
store_fields_direction=self.direction,
122130
)
123-
return mode_mon
131+
return [mode_mon]
124132

125133
def to_mode_solver(self, simulation: Simulation, freqs: FreqArray) -> ModeSolver:
126134
"""Helper to create a :class:`.ModeSolver` instance."""
@@ -136,30 +144,43 @@ def to_mode_solver(self, simulation: Simulation, freqs: FreqArray) -> ModeSolver
136144

137145
def compute_voltage(self, sim_data: SimulationData) -> FreqDataArray:
138146
"""Helper to compute voltage across the port."""
139-
field_monitor = sim_data[self._field_monitor_name]
140-
return self.voltage_integral.compute_voltage(field_monitor)
147+
mode_data = sim_data[self._mode_monitor_name]
148+
voltage_coeffs = self._mode_voltage_coefficients(mode_data)
149+
amps = mode_data.amps
150+
fwd_amps = amps.sel(direction="+").squeeze()
151+
bwd_amps = amps.sel(direction="-").squeeze()
152+
return voltage_coeffs * (fwd_amps + bwd_amps)
141153

142154
def compute_current(self, sim_data: SimulationData) -> FreqDataArray:
143155
"""Helper to compute current flowing through the port."""
144-
field_monitor = sim_data[self._field_monitor_name]
145-
return self.current_integral.compute_current(field_monitor)
156+
mode_data = sim_data[self._mode_monitor_name]
157+
current_coeffs = self._mode_current_coefficients(mode_data)
158+
amps = mode_data.amps
159+
fwd_amps = amps.sel(direction="+").squeeze()
160+
bwd_amps = amps.sel(direction="-").squeeze()
161+
# In ModeData, fwd_amps and bwd_amps are not relative to
162+
# the direction fields are stored
163+
sign = 1.0
164+
if self.direction == "-":
165+
sign = -1.0
166+
return sign * current_coeffs * (fwd_amps - bwd_amps)
146167

147168
def compute_port_impedance(
148-
self, sim_mode_data: Union[SimulationData, ModeSolverData]
169+
self, sim_mode_data: Union[SimulationData, ModeData]
149170
) -> FreqModeDataArray:
150171
"""Helper to compute impedance of port. The port impedance is computed from the
151172
transmission line mode, which should be TEM or at least quasi-TEM."""
152173
impedance_calc = ImpedanceCalculator(
153174
voltage_integral=self.voltage_integral, current_integral=self.current_integral
154175
)
155176
if isinstance(sim_mode_data, SimulationData):
156-
mode_solver_data = sim_mode_data[self._mode_monitor_name]
177+
mode_data = sim_mode_data[self._mode_monitor_name]
157178
else:
158-
mode_solver_data = sim_mode_data
179+
mode_data = sim_mode_data
159180

160181
# Filter out unwanted modes to reduce impedance computation effort
161-
mode_solver_data = mode_solver_data._isel(mode_index=[self.mode_index])
162-
impedance_array = impedance_calc.compute_impedance(mode_solver_data)
182+
mode_data = mode_data._isel(mode_index=[self.mode_index])
183+
impedance_array = impedance_calc.compute_impedance(mode_data)
163184
return impedance_array
164185

165186
@staticmethod
@@ -185,10 +206,11 @@ def _validate_path_integrals_within_port(cls, val, values):
185206
return val
186207

187208
@pd.validator("current_integral", always=True)
209+
@skip_if_fields_missing(["voltage_integral"])
188210
def _check_voltage_or_current(cls, val, values):
189211
"""Raise validation error if both ``voltage_integral`` and ``current_integral``
190212
were not provided."""
191-
if not values.get("voltage_integral") and not val:
213+
if values.get("voltage_integral") is None and val is None:
192214
raise ValidationError(
193215
"At least one of 'voltage_integral' or 'current_integral' must be provided."
194216
)

0 commit comments

Comments
 (0)