Skip to content

Commit a3ac3b5

Browse files
committed
revert to composition and allow a single impedance spec to be used for all modes
1 parent 283b155 commit a3ac3b5

File tree

8 files changed

+121
-83
lines changed

8 files changed

+121
-83
lines changed

tests/test_components/test_microwave.py

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,13 @@ def make_mw_sim(
200200
size_port = [0, sim_width, size_sim[2]]
201201
center_port = [0, 0, center_sim[2]]
202202
impedance_specs = (td.AutoImpedanceSpec(),) * 4
203-
mode_spec = td.MicrowaveModeSpec(
203+
microwave_spec = td.MicrowaveModeSpec(
204+
impedance_specs=impedance_specs,
205+
)
206+
mode_spec = td.ModeSpec(
204207
num_modes=4,
205208
target_neff=1.8,
206-
impedance_specs=impedance_specs,
209+
microwave_spec=microwave_spec,
207210
)
208211

209212
mode_monitor = td.ModeMonitor(
@@ -484,7 +487,12 @@ def test_impedance_spec_validation():
484487
with pytest.raises(pd.ValidationError):
485488
_ = td.CustomImpedanceSpec(voltage_spec=None, current_spec=None)
486489

487-
_ = td.MicrowaveModeSpec(num_modes=4, impedance_specs=(both, voltage_only, current_only, None))
490+
_ = td.ModeSpec(
491+
num_modes=4,
492+
microwave_spec=td.MicrowaveModeSpec(
493+
impedance_specs=(both, voltage_only, current_only, None)
494+
),
495+
)
488496

489497

490498
def test_path_integral_factory_voltage_validation():
@@ -570,21 +578,19 @@ def test_make_path_integrals_validation():
570578
voltage_spec=v_spec,
571579
current_spec=i_spec,
572580
)
573-
microwave_mode_spec = td.MicrowaveModeSpec(impedance_specs=(impedance_spec,))
581+
microwave_spec = td.MicrowaveModeSpec(impedance_specs=(impedance_spec,))
574582

575583
# Test successful creation
576-
voltage_integrals, current_integrals = make_path_integrals(microwave_mode_spec, mode_monitor)
584+
voltage_integrals, current_integrals = make_path_integrals(microwave_spec, mode_monitor)
577585
assert len(voltage_integrals) == 1
578586
assert len(current_integrals) == 1
579587
assert voltage_integrals[0] is not None
580588
assert current_integrals[0] is not None
581589

582590
# Test with None specs - when both are None, use_automatic_setup is True
583591
# This means current integrals will be auto-generated, not None
584-
microwave_mode_spec_none = td.MicrowaveModeSpec(impedance_specs=(None,))
585-
voltage_integrals, current_integrals = make_path_integrals(
586-
microwave_mode_spec_none, mode_monitor
587-
)
592+
microwave_spec_none = td.MicrowaveModeSpec(impedance_specs=(None,))
593+
voltage_integrals, current_integrals = make_path_integrals(microwave_spec_none, mode_monitor)
588594
assert len(voltage_integrals) == mode_monitor.mode_spec.num_modes
589595
assert len(current_integrals) == mode_monitor.mode_spec.num_modes
590596
assert all(vi is None for vi in voltage_integrals)
@@ -601,7 +607,7 @@ def test_make_path_integrals_construction_errors(monkeypatch):
601607
v_spec = td.AxisAlignedVoltageIntegralSpec(center=(1, 2, 3), size=(0, 0, 1), sign="-")
602608

603609
impedance_spec = td.CustomImpedanceSpec(voltage_spec=v_spec, current_spec=None)
604-
microwave_mode_spec = td.MicrowaveModeSpec(impedance_specs=(impedance_spec,))
610+
microwave_spec = td.MicrowaveModeSpec(impedance_specs=(impedance_spec,))
605611

606612
# Mock make_voltage_integral to raise an exception
607613
def mock_make_voltage_integral(path_spec):
@@ -614,7 +620,7 @@ def mock_make_voltage_integral(path_spec):
614620

615621
# This should raise a SetupError due to construction failure
616622
with pytest.raises(SetupError, match="Failed to construct path integrals"):
617-
make_path_integrals(microwave_mode_spec, mode_monitor)
623+
make_path_integrals(microwave_spec, mode_monitor)
618624

619625

620626
def test_path_integral_factory_composite_current():
@@ -662,14 +668,14 @@ def test_path_integral_factory_mixed_specs():
662668
# Test with mixed specs - some None, some specified
663669
impedance_spec1 = td.CustomImpedanceSpec(voltage_spec=v_spec, current_spec=None)
664670
impedance_spec2 = td.CustomImpedanceSpec(voltage_spec=None, current_spec=i_spec)
665-
microwave_mode_spec = td.MicrowaveModeSpec(
666-
num_modes=2,
671+
microwave_spec = td.MicrowaveModeSpec(
667672
impedance_specs=(
668673
impedance_spec1,
669674
impedance_spec2,
670675
),
671676
)
672-
voltage_integrals, current_integrals = make_path_integrals(microwave_mode_spec, mode_monitor)
677+
678+
voltage_integrals, current_integrals = make_path_integrals(microwave_spec, mode_monitor)
673679

674680
assert len(voltage_integrals) == 2
675681
assert len(current_integrals) == 2
@@ -697,11 +703,15 @@ def test_mode_solver_with_microwave_mode_spec():
697703

698704
plane = td.Box(center=(0, 0, 0), size=(0, 10 * width, 2 * height + metal_thickness))
699705
num_modes = 3
700-
impedance_specs = (td.AutoImpedanceSpec(), None, None)
701-
mode_spec = td.MicrowaveModeSpec(
706+
# impedance_specs = (td.AutoImpedanceSpec(), None, None)
707+
impedance_specs = td.AutoImpedanceSpec()
708+
microwave_spec = td.MicrowaveModeSpec(
709+
impedance_specs=impedance_specs,
710+
)
711+
mode_spec = td.ModeSpec(
702712
num_modes=num_modes,
703713
target_neff=2.2,
704-
impedance_specs=impedance_specs,
714+
microwave_spec=microwave_spec,
705715
)
706716
mms = ModeSolver(
707717
simulation=stripline_sim,
@@ -729,7 +739,8 @@ def test_mode_solver_with_microwave_mode_spec():
729739
),
730740
)
731741
impedance_specs = (custom_spec, None, None)
732-
mms = mms.updated_copy(path="mode_spec/", impedance_specs=impedance_specs)
742+
microwave_spec_custom = td.MicrowaveModeSpec(impedance_specs=impedance_specs)
743+
mms = mms.updated_copy(path="mode_spec/", microwave_spec=microwave_spec_custom)
733744
mms_data: ModeSolverData = mms.data
734745

735746
# _, ax = plt.subplots(1, 1, tight_layout=True, figsize=(15, 15))
@@ -738,4 +749,10 @@ def test_mode_solver_with_microwave_mode_spec():
738749
# plt.show()
739750
mms_data.to_dataframe()
740751

741-
assert np.all(np.isclose(mms_data.microwave_data.Z0.real, 28.6, 0.2))
752+
assert np.all(np.isclose(mms_data.microwave_data.Z0.real.sel(mode_index=0), 28.6, 0.2))
753+
754+
# Make sure a single spec can be used
755+
microwave_spec_custom = td.MicrowaveModeSpec(impedance_specs=custom_spec)
756+
mms = mms.updated_copy(path="mode_spec/", microwave_spec=microwave_spec_custom)
757+
mms_data: ModeSolverData = mms.data
758+
assert np.all(np.isclose(mms_data.microwave_data.Z0.real.sel(mode_index=0), 28.6, 0.2))

tests/test_components/test_mode.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -394,10 +394,12 @@ def test_mode_spec_with_microwave_mode_spec():
394394
impedance_specs = (td.AutoImpedanceSpec(),)
395395

396396
# Should work when impedance_specs matches num_modes
397-
mw_mode_spec = td.MicrowaveModeSpec(num_modes=1, impedance_specs=impedance_specs)
398-
assert mw_mode_spec.num_modes == 1
399-
assert len(mw_mode_spec.impedance_specs) == 1
397+
mw_spec = td.MicrowaveModeSpec(impedance_specs=impedance_specs)
398+
mode_spec = td.ModeSpec(num_modes=1, microwave_spec=mw_spec)
399+
assert mode_spec.num_modes == 1
400+
assert len(mode_spec.microwave_spec.impedance_specs) == 1
400401

401402
# Should fail when impedance_specs doesn't match num_modes
402403
with pytest.raises(pydantic.ValidationError):
403-
td.MicrowaveModeSpec(num_modes=2, impedance_specs=impedance_specs)
404+
mw_spec_2 = td.MicrowaveModeSpec(impedance_specs=impedance_specs)
405+
td.ModeSpec(num_modes=2, microwave_spec=mw_spec_2)

tidy3d/components/microwave/microwave_mode_spec.py

Lines changed: 23 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import annotations
44

5-
from typing import Optional
5+
from typing import Optional, Union
66

77
import pydantic.v1 as pd
88

@@ -12,12 +12,10 @@
1212
AutoImpedanceSpec,
1313
ImpedanceSpecTypes,
1414
)
15-
from tidy3d.components.mode_spec import ModeSpec
1615
from tidy3d.components.types import annotate_type
17-
from tidy3d.exceptions import SetupError
1816

1917

20-
class MicrowaveModeSpec(MicrowaveBaseModel, ModeSpec):
18+
class MicrowaveModeSpec(MicrowaveBaseModel):
2119
"""
2220
The :class:`.MicrowaveModeSpec` class specifies how quantities related to transmission line
2321
modes and microwave waveguides are computed. For example, it defines the paths for line integrals, which are used to
@@ -26,13 +24,12 @@ class MicrowaveModeSpec(MicrowaveBaseModel, ModeSpec):
2624
Example
2725
-------
2826
>>> import tidy3d as td
29-
>>> # Using automatic impedance calculation
30-
>>> mode_spec_auto = td.MicrowaveModeSpec(
27+
>>> # Using automatic impedance calculation (single spec, will be duplicated for all modes)
28+
>>> mode_spec_auto = td.ModeSpec(
3129
... num_modes=2,
32-
... target_neff=1.5,
33-
... impedance_specs=(td.AutoImpedanceSpec(), td.AutoImpedanceSpec())
30+
... microwave_spec=td.MicrowaveModeSpec(impedance_specs=td.AutoImpedanceSpec())
3431
... )
35-
>>> # Using custom impedance specification
32+
>>> # Using custom impedance specification for multiple modes
3633
>>> voltage_spec = td.AxisAlignedVoltageIntegralSpec(
3734
... center=(0, 0, 0), size=(0, 0, 1), sign="+"
3835
... )
@@ -42,47 +39,36 @@ class MicrowaveModeSpec(MicrowaveBaseModel, ModeSpec):
4239
>>> custom_impedance = td.CustomImpedanceSpec(
4340
... voltage_spec=voltage_spec, current_spec=current_spec
4441
... )
45-
>>> mode_spec_custom = td.MicrowaveModeSpec(
42+
>>> mode_spec_custom = td.ModeSpec(
4643
... num_modes=1,
47-
... impedance_specs=(custom_impedance,)
44+
... microwave_spec=td.MicrowaveModeSpec(impedance_specs=custom_impedance)
4845
... )
4946
"""
5047

51-
impedance_specs: tuple[Optional[annotate_type(ImpedanceSpecTypes)], ...] = pd.Field(
48+
impedance_specs: Union[
49+
annotate_type(ImpedanceSpecTypes),
50+
tuple[Optional[annotate_type(ImpedanceSpecTypes)], ...],
51+
] = pd.Field(
5252
...,
5353
title="Impedance Specifications",
5454
description="Field controls how the impedance is calculated for each mode calculated by the mode solver. "
55-
"The number of impedance specifications should match the number of modes field. "
55+
"Can be a single impedance specification (which will be applied to all modes) or a tuple of specifications "
56+
"(one per mode). The number of impedance specifications should match the number of modes field. "
5657
"When an impedance specification of ``None`` is used, the impedance calculation will be "
5758
"ignored for the associated mode.",
5859
)
5960

61+
@cached_property
62+
def _impedance_specs_as_tuple(self) -> tuple[Optional[ImpedanceSpecTypes]]:
63+
"""Gets the impedance_specs field converted to a tuple."""
64+
if isinstance(self.impedance_specs, Union[tuple, list]):
65+
return tuple(self.impedance_specs)
66+
return (self.impedance_specs,)
67+
6068
@cached_property
6169
def _using_auto_current_spec(self) -> bool:
6270
"""Checks whether at least one of the modes will require an auto setup of the current path specification."""
6371
return any(
64-
isinstance(impedance_spec, AutoImpedanceSpec) for impedance_spec in self.impedance_specs
72+
isinstance(impedance_spec, AutoImpedanceSpec)
73+
for impedance_spec in self._impedance_specs_as_tuple
6574
)
66-
67-
@cached_property
68-
def num_impedance_specs(self) -> int:
69-
"""The number of impedance specifications to be used."""
70-
return len(self.impedance_specs)
71-
72-
@pd.validator("impedance_specs", always=True)
73-
def check_impedance_specs_consistent_with_num_modes(cls, val, values):
74-
"""Check that the number of impedance specifications is equal to the number of modes."""
75-
if val is None:
76-
return val
77-
78-
num_modes = values.get("num_modes", 1)
79-
valid_number_impedance_specs = len(val) == num_modes
80-
81-
if not valid_number_impedance_specs:
82-
raise SetupError(
83-
f"Given {len(val)} impedance specifications in the 'MicrowaveModeSpec', "
84-
f"but the number of modes requested is {num_modes}. Please ensure that the "
85-
"number of impedance specifications is equal to the number of modes."
86-
)
87-
88-
return val

tidy3d/components/microwave/path_integrals/path_integral_factory.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def make_path_integrals(
8686
monitor: Union[ModeMonitor, ModeSolverMonitor],
8787
) -> tuple[tuple[Optional[VoltageIntegralTypes]], tuple[Optional[CurrentIntegralTypes]]]:
8888
"""
89-
Given an impedance specification and monitor, create the voltage and
89+
Given a microwave mode specification and monitor, create the voltage and
9090
current path integrals used for the impedance computation.
9191
9292
Parameters
@@ -112,7 +112,11 @@ def make_path_integrals(
112112

113113
v_integrals = []
114114
i_integrals = []
115-
for idx, impedance_spec in enumerate(microwave_mode_spec.impedance_specs):
115+
116+
# Handle case where impedance spec is a single ImpedanceSpecTypes
117+
impedance_specs = microwave_mode_spec._impedance_specs_as_tuple
118+
119+
for idx, impedance_spec in enumerate(impedance_specs):
116120
if impedance_spec is None:
117121
# Do not calculate impedance for this mode
118122
v_integrals.append(None)

tidy3d/components/mode/mode_solver.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,9 @@
3535
LossyMetalMedium,
3636
)
3737
from tidy3d.components.microwave.data.dataset import MicrowaveModeDataset
38-
from tidy3d.components.microwave.microwave_mode_spec import MicrowaveModeSpec
3938
from tidy3d.components.microwave.path_integrals.path_integral_factory import make_path_integrals
4039
from tidy3d.components.mode_spec import ModeSpec
41-
from tidy3d.components.monitor import ModeMonitor, ModeSolverMonitor, ModeSpecTypes
40+
from tidy3d.components.monitor import ModeMonitor, ModeSolverMonitor
4241
from tidy3d.components.scene import Scene
4342
from tidy3d.components.simulation import Simulation
4443
from tidy3d.components.source.field import ModeSource
@@ -157,7 +156,7 @@ class ModeSolver(Tidy3dBaseModel):
157156
discriminator=TYPE_TAG_STR,
158157
)
159158

160-
mode_spec: ModeSpecTypes = pydantic.Field(
159+
mode_spec: ModeSpec = pydantic.Field(
161160
...,
162161
title="Mode specification",
163162
description="Container with specifications about the modes to be solved for.",
@@ -439,9 +438,9 @@ def _num_cells_freqs_modes(self) -> tuple[int, int, int]:
439438
return num_cells, num_freqs, num_modes
440439

441440
@property
442-
def _is_microwave_mode_spec(self) -> bool:
443-
"""Check if the mode_spec is a MicrowaveModeSpec."""
444-
return isinstance(self.mode_spec, MicrowaveModeSpec)
441+
def _has_microwave_mode_spec(self) -> bool:
442+
"""Check if the mode_spec is using a MicrowaveModeSpec."""
443+
return self.mode_spec.microwave_spec is not None
445444

446445
def solve(self) -> ModeSolverData:
447446
""":class:`.ModeSolverData` containing the field and effective index data.
@@ -550,7 +549,7 @@ def data_raw(self) -> ModeSolverData:
550549

551550
mode_solver_data = self._filter_components(mode_solver_data)
552551
# Calculate and add the characteristic impedance
553-
if self._is_microwave_mode_spec:
552+
if self._has_microwave_mode_spec:
554553
mode_solver_data = self._add_microwave_data(mode_solver_data)
555554
return mode_solver_data
556555

@@ -1377,10 +1376,12 @@ def _make_path_integrals(
13771376
) -> tuple[tuple[Optional[VoltageIntegralTypes]], tuple[Optional[CurrentIntegralTypes]]]:
13781377
"""Wrapper for making path integrals from the MicrowaveModeSpec. Note: overriden in the backend to support
13791378
auto creation of path integrals."""
1380-
if not self._is_microwave_mode_spec:
1381-
raise ValueError("Cannot make path integrals for non-MicrowaveModeSpec")
1379+
if not self._has_microwave_mode_spec:
1380+
raise ValueError(
1381+
"Cannot make path integrals for when 'mode_spec' is not a 'MicrowaveModeSpec'."
1382+
)
13821383
return make_path_integrals(
1383-
self.mode_spec,
1384+
self.mode_spec.microwave_spec,
13841385
self.to_monitor(name=MODE_MONITOR_NAME),
13851386
)
13861387

@@ -1392,6 +1393,9 @@ def _add_microwave_data(self, mode_solver_data: ModeSolverData) -> ModeSolverDat
13921393
Z0_list = []
13931394
V_list = []
13941395
I_list = []
1396+
if len(voltage_integrals) == 1 and self.mode_spec.num_modes > 1:
1397+
voltage_integrals = voltage_integrals * self.mode_spec.num_modes
1398+
current_integrals = current_integrals * self.mode_spec.num_modes
13951399
for mode_index in range(self.mode_spec.num_modes):
13961400
vi = voltage_integrals[mode_index]
13971401
ci = current_integrals[mode_index]

0 commit comments

Comments
 (0)