diff --git a/schemas/Simulation.json b/schemas/Simulation.json index 6ee845b7ca..7bd1ec2cda 100644 --- a/schemas/Simulation.json +++ b/schemas/Simulation.json @@ -817,6 +817,248 @@ ], "type": "object" }, + "AstigmaticGaussianOverlapMonitor": { + "additionalProperties": false, + "properties": { + "angle_phi": { + "default": 0.0, + "type": "number" + }, + "angle_theta": { + "default": 0.0, + "type": "number" + }, + "apodization": { + "allOf": [ + { + "$ref": "#/definitions/ApodizationSpec" + } + ], + "default": { + "attrs": {}, + "end": null, + "start": null, + "type": "ApodizationSpec", + "width": null + } + }, + "attrs": { + "default": {}, + "type": "object" + }, + "center": { + "anyOf": [ + { + "items": [ + { + "anyOf": [ + { + "type": "autograd.tracer.Box" + }, + { + "type": "number" + } + ] + }, + { + "anyOf": [ + { + "type": "autograd.tracer.Box" + }, + { + "type": "number" + } + ] + }, + { + "anyOf": [ + { + "type": "autograd.tracer.Box" + }, + { + "type": "number" + } + ] + } + ], + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + { + "type": "autograd.tracer.Box" + } + ], + "default": [ + 0.0, + 0.0, + 0.0 + ] + }, + "colocate": { + "default": true, + "type": "boolean" + }, + "conjugated_dot_product": { + "default": true, + "type": "boolean" + }, + "freqs": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "type": "array" + }, + { + "type": "ArrayLike" + } + ] + }, + "interval_space": { + "default": [ + 1, + 1, + 1 + ], + "items": [ + { + "enum": [ + 1 + ], + "type": "integer" + }, + { + "enum": [ + 1 + ], + "type": "integer" + }, + { + "enum": [ + 1 + ], + "type": "integer" + } + ], + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + "name": { + "minLength": 1, + "type": "string" + }, + "pol_angle": { + "default": 0, + "type": "number" + }, + "size": { + "anyOf": [ + { + "items": [ + { + "anyOf": [ + { + "minimum": 0, + "type": "number" + }, + { + "type": "autograd.tracer.Box" + } + ] + }, + { + "anyOf": [ + { + "minimum": 0, + "type": "number" + }, + { + "type": "autograd.tracer.Box" + } + ] + }, + { + "anyOf": [ + { + "minimum": 0, + "type": "number" + }, + { + "type": "autograd.tracer.Box" + } + ] + } + ], + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + { + "type": "autograd.tracer.Box" + } + ] + }, + "store_fields_direction": { + "enum": [ + "+", + "-" + ], + "type": "string" + }, + "type": { + "default": "AstigmaticGaussianOverlapMonitor", + "enum": [ + "AstigmaticGaussianOverlapMonitor" + ], + "type": "string" + }, + "waist_distances": { + "default": [ + 0.0, + 0.0 + ], + "items": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "maxItems": 2, + "minItems": 2, + "type": "array" + }, + "waist_sizes": { + "default": [ + 1.0, + 1.0 + ], + "items": [ + { + "exclusiveMinimum": 0, + "type": "number" + }, + { + "exclusiveMinimum": 0, + "type": "number" + } + ], + "maxItems": 2, + "minItems": 2, + "type": "array" + } + }, + "required": [ + "freqs", + "name", + "size" + ], + "type": "object" + }, "AugerRecombination": { "additionalProperties": false, "properties": { @@ -8299,6 +8541,221 @@ ], "type": "object" }, + "GaussianOverlapMonitor": { + "additionalProperties": false, + "properties": { + "angle_phi": { + "default": 0.0, + "type": "number" + }, + "angle_theta": { + "default": 0.0, + "type": "number" + }, + "apodization": { + "allOf": [ + { + "$ref": "#/definitions/ApodizationSpec" + } + ], + "default": { + "attrs": {}, + "end": null, + "start": null, + "type": "ApodizationSpec", + "width": null + } + }, + "attrs": { + "default": {}, + "type": "object" + }, + "center": { + "anyOf": [ + { + "items": [ + { + "anyOf": [ + { + "type": "autograd.tracer.Box" + }, + { + "type": "number" + } + ] + }, + { + "anyOf": [ + { + "type": "autograd.tracer.Box" + }, + { + "type": "number" + } + ] + }, + { + "anyOf": [ + { + "type": "autograd.tracer.Box" + }, + { + "type": "number" + } + ] + } + ], + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + { + "type": "autograd.tracer.Box" + } + ], + "default": [ + 0.0, + 0.0, + 0.0 + ] + }, + "colocate": { + "default": true, + "type": "boolean" + }, + "conjugated_dot_product": { + "default": true, + "type": "boolean" + }, + "freqs": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "type": "array" + }, + { + "type": "ArrayLike" + } + ] + }, + "interval_space": { + "default": [ + 1, + 1, + 1 + ], + "items": [ + { + "enum": [ + 1 + ], + "type": "integer" + }, + { + "enum": [ + 1 + ], + "type": "integer" + }, + { + "enum": [ + 1 + ], + "type": "integer" + } + ], + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + "name": { + "minLength": 1, + "type": "string" + }, + "pol_angle": { + "default": 0, + "type": "number" + }, + "size": { + "anyOf": [ + { + "items": [ + { + "anyOf": [ + { + "minimum": 0, + "type": "number" + }, + { + "type": "autograd.tracer.Box" + } + ] + }, + { + "anyOf": [ + { + "minimum": 0, + "type": "number" + }, + { + "type": "autograd.tracer.Box" + } + ] + }, + { + "anyOf": [ + { + "minimum": 0, + "type": "number" + }, + { + "type": "autograd.tracer.Box" + } + ] + } + ], + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + { + "type": "autograd.tracer.Box" + } + ] + }, + "store_fields_direction": { + "enum": [ + "+", + "-" + ], + "type": "string" + }, + "type": { + "default": "GaussianOverlapMonitor", + "enum": [ + "GaussianOverlapMonitor" + ], + "type": "string" + }, + "waist_distance": { + "default": 0.0, + "type": "number" + }, + "waist_radius": { + "default": 1.0, + "exclusiveMinimum": 0, + "type": "number" + } + }, + "required": [ + "freqs", + "name", + "size" + ], + "type": "object" + }, "GaussianPulse": { "additionalProperties": false, "properties": { @@ -16745,6 +17202,7 @@ "items": { "discriminator": { "mapping": { + "AstigmaticGaussianOverlapMonitor": "#/definitions/AstigmaticGaussianOverlapMonitor", "AuxFieldTimeMonitor": "#/definitions/AuxFieldTimeMonitor", "DiffractionMonitor": "#/definitions/DiffractionMonitor", "DirectivityMonitor": "#/definitions/DirectivityMonitor", @@ -16755,6 +17213,7 @@ "FieldTimeMonitor": "#/definitions/FieldTimeMonitor", "FluxMonitor": "#/definitions/FluxMonitor", "FluxTimeMonitor": "#/definitions/FluxTimeMonitor", + "GaussianOverlapMonitor": "#/definitions/GaussianOverlapMonitor", "MediumMonitor": "#/definitions/MediumMonitor", "MicrowaveModeMonitor": "#/definitions/MicrowaveModeMonitor", "MicrowaveModeSolverMonitor": "#/definitions/MicrowaveModeSolverMonitor", @@ -16765,6 +17224,9 @@ "propertyName": "type" }, "oneOf": [ + { + "$ref": "#/definitions/AstigmaticGaussianOverlapMonitor" + }, { "$ref": "#/definitions/AuxFieldTimeMonitor" }, @@ -16795,6 +17257,9 @@ { "$ref": "#/definitions/FluxTimeMonitor" }, + { + "$ref": "#/definitions/GaussianOverlapMonitor" + }, { "$ref": "#/definitions/MediumMonitor" }, diff --git a/schemas/TerminalComponentModeler.json b/schemas/TerminalComponentModeler.json index 75ac360b7b..e124290427 100644 --- a/schemas/TerminalComponentModeler.json +++ b/schemas/TerminalComponentModeler.json @@ -817,6 +817,248 @@ ], "type": "object" }, + "AstigmaticGaussianOverlapMonitor": { + "additionalProperties": false, + "properties": { + "angle_phi": { + "default": 0.0, + "type": "number" + }, + "angle_theta": { + "default": 0.0, + "type": "number" + }, + "apodization": { + "allOf": [ + { + "$ref": "#/definitions/ApodizationSpec" + } + ], + "default": { + "attrs": {}, + "end": null, + "start": null, + "type": "ApodizationSpec", + "width": null + } + }, + "attrs": { + "default": {}, + "type": "object" + }, + "center": { + "anyOf": [ + { + "items": [ + { + "anyOf": [ + { + "type": "autograd.tracer.Box" + }, + { + "type": "number" + } + ] + }, + { + "anyOf": [ + { + "type": "autograd.tracer.Box" + }, + { + "type": "number" + } + ] + }, + { + "anyOf": [ + { + "type": "autograd.tracer.Box" + }, + { + "type": "number" + } + ] + } + ], + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + { + "type": "autograd.tracer.Box" + } + ], + "default": [ + 0.0, + 0.0, + 0.0 + ] + }, + "colocate": { + "default": true, + "type": "boolean" + }, + "conjugated_dot_product": { + "default": true, + "type": "boolean" + }, + "freqs": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "type": "array" + }, + { + "type": "ArrayLike" + } + ] + }, + "interval_space": { + "default": [ + 1, + 1, + 1 + ], + "items": [ + { + "enum": [ + 1 + ], + "type": "integer" + }, + { + "enum": [ + 1 + ], + "type": "integer" + }, + { + "enum": [ + 1 + ], + "type": "integer" + } + ], + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + "name": { + "minLength": 1, + "type": "string" + }, + "pol_angle": { + "default": 0, + "type": "number" + }, + "size": { + "anyOf": [ + { + "items": [ + { + "anyOf": [ + { + "minimum": 0, + "type": "number" + }, + { + "type": "autograd.tracer.Box" + } + ] + }, + { + "anyOf": [ + { + "minimum": 0, + "type": "number" + }, + { + "type": "autograd.tracer.Box" + } + ] + }, + { + "anyOf": [ + { + "minimum": 0, + "type": "number" + }, + { + "type": "autograd.tracer.Box" + } + ] + } + ], + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + { + "type": "autograd.tracer.Box" + } + ] + }, + "store_fields_direction": { + "enum": [ + "+", + "-" + ], + "type": "string" + }, + "type": { + "default": "AstigmaticGaussianOverlapMonitor", + "enum": [ + "AstigmaticGaussianOverlapMonitor" + ], + "type": "string" + }, + "waist_distances": { + "default": [ + 0.0, + 0.0 + ], + "items": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "maxItems": 2, + "minItems": 2, + "type": "array" + }, + "waist_sizes": { + "default": [ + 1.0, + 1.0 + ], + "items": [ + { + "exclusiveMinimum": 0, + "type": "number" + }, + { + "exclusiveMinimum": 0, + "type": "number" + } + ], + "maxItems": 2, + "minItems": 2, + "type": "array" + } + }, + "required": [ + "freqs", + "name", + "size" + ], + "type": "object" + }, "AugerRecombination": { "additionalProperties": false, "properties": { @@ -8780,6 +9022,221 @@ ], "type": "object" }, + "GaussianOverlapMonitor": { + "additionalProperties": false, + "properties": { + "angle_phi": { + "default": 0.0, + "type": "number" + }, + "angle_theta": { + "default": 0.0, + "type": "number" + }, + "apodization": { + "allOf": [ + { + "$ref": "#/definitions/ApodizationSpec" + } + ], + "default": { + "attrs": {}, + "end": null, + "start": null, + "type": "ApodizationSpec", + "width": null + } + }, + "attrs": { + "default": {}, + "type": "object" + }, + "center": { + "anyOf": [ + { + "items": [ + { + "anyOf": [ + { + "type": "autograd.tracer.Box" + }, + { + "type": "number" + } + ] + }, + { + "anyOf": [ + { + "type": "autograd.tracer.Box" + }, + { + "type": "number" + } + ] + }, + { + "anyOf": [ + { + "type": "autograd.tracer.Box" + }, + { + "type": "number" + } + ] + } + ], + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + { + "type": "autograd.tracer.Box" + } + ], + "default": [ + 0.0, + 0.0, + 0.0 + ] + }, + "colocate": { + "default": true, + "type": "boolean" + }, + "conjugated_dot_product": { + "default": true, + "type": "boolean" + }, + "freqs": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "type": "array" + }, + { + "type": "ArrayLike" + } + ] + }, + "interval_space": { + "default": [ + 1, + 1, + 1 + ], + "items": [ + { + "enum": [ + 1 + ], + "type": "integer" + }, + { + "enum": [ + 1 + ], + "type": "integer" + }, + { + "enum": [ + 1 + ], + "type": "integer" + } + ], + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + "name": { + "minLength": 1, + "type": "string" + }, + "pol_angle": { + "default": 0, + "type": "number" + }, + "size": { + "anyOf": [ + { + "items": [ + { + "anyOf": [ + { + "minimum": 0, + "type": "number" + }, + { + "type": "autograd.tracer.Box" + } + ] + }, + { + "anyOf": [ + { + "minimum": 0, + "type": "number" + }, + { + "type": "autograd.tracer.Box" + } + ] + }, + { + "anyOf": [ + { + "minimum": 0, + "type": "number" + }, + { + "type": "autograd.tracer.Box" + } + ] + } + ], + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + { + "type": "autograd.tracer.Box" + } + ] + }, + "store_fields_direction": { + "enum": [ + "+", + "-" + ], + "type": "string" + }, + "type": { + "default": "GaussianOverlapMonitor", + "enum": [ + "GaussianOverlapMonitor" + ], + "type": "string" + }, + "waist_distance": { + "default": 0.0, + "type": "number" + }, + "waist_radius": { + "default": 1.0, + "exclusiveMinimum": 0, + "type": "number" + } + }, + "required": [ + "freqs", + "name", + "size" + ], + "type": "object" + }, "GaussianPulse": { "additionalProperties": false, "properties": { @@ -15657,6 +16114,7 @@ "items": { "discriminator": { "mapping": { + "AstigmaticGaussianOverlapMonitor": "#/definitions/AstigmaticGaussianOverlapMonitor", "AuxFieldTimeMonitor": "#/definitions/AuxFieldTimeMonitor", "DiffractionMonitor": "#/definitions/DiffractionMonitor", "DirectivityMonitor": "#/definitions/DirectivityMonitor", @@ -15667,6 +16125,7 @@ "FieldTimeMonitor": "#/definitions/FieldTimeMonitor", "FluxMonitor": "#/definitions/FluxMonitor", "FluxTimeMonitor": "#/definitions/FluxTimeMonitor", + "GaussianOverlapMonitor": "#/definitions/GaussianOverlapMonitor", "MediumMonitor": "#/definitions/MediumMonitor", "MicrowaveModeMonitor": "#/definitions/MicrowaveModeMonitor", "MicrowaveModeSolverMonitor": "#/definitions/MicrowaveModeSolverMonitor", @@ -15677,6 +16136,9 @@ "propertyName": "type" }, "oneOf": [ + { + "$ref": "#/definitions/AstigmaticGaussianOverlapMonitor" + }, { "$ref": "#/definitions/AuxFieldTimeMonitor" }, @@ -15707,6 +16169,9 @@ { "$ref": "#/definitions/FluxTimeMonitor" }, + { + "$ref": "#/definitions/GaussianOverlapMonitor" + }, { "$ref": "#/definitions/MediumMonitor" }, diff --git a/tests/test_components/test_monitor.py b/tests/test_components/test_monitor.py index b8b9faa60b..ae823303d3 100644 --- a/tests/test_components/test_monitor.py +++ b/tests/test_components/test_monitor.py @@ -344,6 +344,14 @@ def test_diffraction_validators(): FREQS = np.array([1, 2, 3]) * 1e12 +def test_gaussian_overlap_monitors_basic(): + g = td.GaussianOverlapMonitor(size=(1, 1, 0), name="g", freqs=FREQS) + a = td.AstigmaticGaussianOverlapMonitor(size=(1, 1, 0), name="a", freqs=FREQS) + for m in (g, a): + s = m.storage_size(num_cells=10, tmesh=[0.0, 1.0]) + assert isinstance(s, int) and s > 0 + + def test_monitor(): size = (1, 2, 3) center = (1, 2, 3) @@ -381,10 +389,14 @@ def test_monitor(): m10 = td.PermittivityMonitor(size=size, center=center, freqs=FREQS, name="perm") m11 = td.AuxFieldTimeMonitor(size=size, center=center, name="aux_field_time", fields=("Nfx",)) m12 = td.MediumMonitor(size=size, center=center, freqs=FREQS, name="mat") + m13 = td.GaussianOverlapMonitor(size=(1, 1, 0), center=center, freqs=FREQS, name="gauss") + m14 = td.AstigmaticGaussianOverlapMonitor( + size=(1, 1, 0), center=center, freqs=FREQS, name="astigauss" + ) tmesh = np.linspace(0, 1, 10) - for m in [m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m12]: + for m in [m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14]: m.storage_size(num_cells=100, tmesh=tmesh) for m in [m2, m4]: diff --git a/tests/test_data/test_monitor_data.py b/tests/test_data/test_monitor_data.py index b1abe24127..98f57fbb90 100644 --- a/tests/test_data/test_monitor_data.py +++ b/tests/test_data/test_monitor_data.py @@ -18,6 +18,7 @@ DiffractionData, DirectivityData, FieldData, + FieldOverlapData, FieldTimeData, FluxData, FluxTimeData, @@ -223,6 +224,16 @@ def make_mode_data(): return ModeData(monitor=MODE_MONITOR, amps=AMPS.copy(), n_complex=N_COMPLEX.copy()) +def make_field_overlap_data(): + monitor = td.GaussianOverlapMonitor( + size=(0, 2, 2), + freqs=[1e14, 1.1e14], + name="gaussian_overlap_monitor", + store_fields_direction="+", + ) + return FieldOverlapData(monitor=monitor, amps=AMPS) + + def make_flux_data(): return FluxData(monitor=FLUX_MONITOR, flux=FLUX.copy()) @@ -412,6 +423,11 @@ def test_mode_data(): _ = data.k_eff +def test_overlap_data(): + data = make_field_overlap_data() + _ = data.amps + + def test_flux_data(): data = make_flux_data() _ = data.flux diff --git a/tests/test_plugins/smatrix/test_component_modeler.py b/tests/test_plugins/smatrix/test_component_modeler.py index 778adacd41..b4b7afa2e8 100644 --- a/tests/test_plugins/smatrix/test_component_modeler.py +++ b/tests/test_plugins/smatrix/test_component_modeler.py @@ -9,7 +9,13 @@ import tidy3d as td from tidy3d import SimulationDataMap from tidy3d.exceptions import SetupError, Tidy3dKeyError -from tidy3d.plugins.smatrix import ModalComponentModeler, ModalComponentModelerData, Port +from tidy3d.plugins.smatrix import ( + AstigmaticGaussianPort, + GaussianPort, + ModalComponentModeler, + ModalComponentModelerData, + Port, +) from tidy3d.web.api.container import Batch from ...utils import run_emulated @@ -144,6 +150,7 @@ def offset(u): def make_ports(): sim = make_coupler() + # source src_pos = sim.size[0] / 2 - straight_wg_length / 2 @@ -179,7 +186,28 @@ def make_ports(): name="left_bot", ) - return [port_right_top, port_right_bot, port_left_top, port_left_bot] + # Gaussian ports on top and bottom + port_z_bot = AstigmaticGaussianPort( + center=[0, 0, wg_height + 0.1], + size=(10, 10, 0), + direction="-", + name="z_top", + angle_theta=0.0, + angle_phi=0.0, + pol_angle=0.0, + ) + + port_z_top = GaussianPort( + center=[0, 0, -0.1], + size=(10, 10, 0), + direction="+", + name="z_bot", + angle_theta=0.0, + angle_phi=0.0, + pol_angle=0.0, + ) + + return [port_right_top, port_right_bot, port_left_top, port_left_bot, port_z_bot, port_z_top] def make_component_modeler(**kwargs): @@ -283,13 +311,12 @@ def test_run_component_modeler(monkeypatch): s_matrix = modeler_data.smatrix() for port_in in modeler.ports: - for mode_index_in in range(port_in.mode_spec.num_modes): + for mode_index_in in range(port_in.num_modes): for port_out in modeler.ports: - for mode_index_out in range(port_out.mode_spec.num_modes): + for mode_index_out in range(port_out.num_modes): coords_in = {"port_in": port_in.name, "mode_index_in": mode_index_in} coords_out = {"port_out": port_out.name, "mode_index_out": mode_index_out} - - assert np.all(s_matrix.sel(**coords_in) != 0), ( + assert np.all(s_matrix.sel(**coords_in).sel(mode_index_out=0) != 0), ( "source index not present in S matrix" ) assert np.all(s_matrix.sel(**coords_in).sel(**coords_out) != 0), ( @@ -309,7 +336,7 @@ def test_component_modeler_run_only(monkeypatch): coords_in_run_only = {"port_in": port_run_only, "mode_index_in": mode_index_run_only} # make sure the run only mappings are non-zero - assert np.all(s_matrix.sel(**coords_in_run_only) != 0) + assert np.all(s_matrix.sel(**coords_in_run_only).sel(mode_index_out=0) != 0) # make sure if we zero out the run_only mappings, everythging is zero s_matrix.loc[coords_in_run_only] = 0 @@ -370,7 +397,7 @@ def test_mapping_exclusion(monkeypatch): # add a mapping to each element in the row of EXCLUDE_INDEX for port in ports: - for mode_index in range(port.mode_spec.num_modes): + for mode_index in range(port.num_modes): row_index = (port.name, mode_index) if row_index != EXCLUDE_INDEX: mapping = ((row_index, row_index), (row_index, EXCLUDE_INDEX), +1) @@ -400,7 +427,7 @@ def test_mapping_with_run_only(): run_only = [] # add a mapping to each element in the row of EXCLUDE_INDEX for port in ports: - for mode_index in range(port.mode_spec.num_modes): + for mode_index in range(port.num_modes): # Test that providing a list is properly handled row_index = [port.name, mode_index] run_only.append(row_index) diff --git a/tests/utils.py b/tests/utils.py index 4f67ddf5c0..9131a868ac 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1297,6 +1297,43 @@ def make_mode_data(monitor: td.ModeMonitor) -> td.ModeData: **field_cmps, ) + # New: Gaussian-like overlap monitor data + def make_gaussian_overlap_data(monitor: td.GaussianOverlapMonitor) -> td.FieldOverlapData: + """Random FieldOverlapData from a GaussianOverlapMonitor.""" + grid = simulation.discretize_monitor(monitor) + coords_amps = { + "direction": ["+", "-"], + "f": list(monitor.freqs), + "mode_index": [0], # singleton for Gaussian ports + } + amps = make_data(coords=coords_amps, data_array_type=td.ModeAmpsDataArray, is_complex=True) + return td.FieldOverlapData( + monitor=monitor, + amps=amps, + symmetry=(0, 0, 0), + symmetry_center=simulation.center, + grid_expanded=grid, + ) + + def make_astig_gaussian_overlap_data( + monitor: td.AstigmaticGaussianOverlapMonitor, + ) -> td.FieldOverlapData: + """Random FieldOverlapData from an AstigmaticGaussianOverlapMonitor.""" + grid = simulation.discretize_monitor(monitor) + coords_amps = { + "direction": ["+", "-"], + "f": list(monitor.freqs), + "mode_index": [0], # singleton for Gaussian ports + } + amps = make_data(coords=coords_amps, data_array_type=td.ModeAmpsDataArray, is_complex=True) + return td.FieldOverlapData( + monitor=monitor, + amps=amps, + symmetry=(0, 0, 0), + symmetry_center=simulation.center, + grid_expanded=grid, + ) + def make_flux_data(monitor: td.FluxMonitor) -> td.FluxData: """make a random ModeData from a ModeMonitor.""" @@ -1480,6 +1517,8 @@ def make_flux_time_data(monitor: td.FluxTimeMonitor) -> td.FluxTimeData: td.FieldProjectionKSpaceMonitor: make_field_projection_kspace_data, td.AuxFieldTimeMonitor: make_aux_field_time_data, td.FluxTimeMonitor: make_flux_time_data, + td.GaussianOverlapMonitor: make_gaussian_overlap_data, + td.AstigmaticGaussianOverlapMonitor: make_astig_gaussian_overlap_data, } data = [MONITOR_MAKER_MAP[type(mnt)](mnt) for mnt in simulation.monitors] diff --git a/tidy3d/__init__.py b/tidy3d/__init__.py index 21ebd2d5ac..a48c9ba481 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -220,6 +220,7 @@ DiffractionData, DirectivityData, FieldData, + FieldOverlapData, FieldProjectionAngleData, FieldProjectionCartesianData, FieldProjectionKSpaceData, @@ -345,6 +346,7 @@ # monitors from .components.monitor import ( + AstigmaticGaussianOverlapMonitor, AuxFieldTimeMonitor, DiffractionMonitor, DirectivityMonitor, @@ -356,6 +358,7 @@ FieldTimeMonitor, FluxMonitor, FluxTimeMonitor, + GaussianOverlapMonitor, MediumMonitor, ModeMonitor, ModeSolverMonitor, @@ -492,6 +495,7 @@ def set_logging_level(level: str) -> None: "ApodizationSpec", "AstigmaticGaussianBeam", "AstigmaticGaussianBeamProfile", + "AstigmaticGaussianOverlapMonitor", "AugerRecombination", "AutoGrid", "AutoImpedanceSpec", @@ -600,6 +604,7 @@ def set_logging_level(level: str) -> None: "FieldDataset", "FieldGrid", "FieldMonitor", + "FieldOverlapData", "FieldProjectionAngleData", "FieldProjectionAngleDataArray", "FieldProjectionAngleMonitor", @@ -631,6 +636,7 @@ def set_logging_level(level: str) -> None: "GaussianBeam", "GaussianBeamProfile", "GaussianDoping", + "GaussianOverlapMonitor", "GaussianPulse", "Geometry", "GeometryGroup", diff --git a/tidy3d/components/data/monitor_data.py b/tidy3d/components/data/monitor_data.py index ccb31ca278..26c2325c0b 100644 --- a/tidy3d/components/data/monitor_data.py +++ b/tidy3d/components/data/monitor_data.py @@ -19,6 +19,7 @@ from tidy3d.components.grid.grid import Coords, Grid from tidy3d.components.medium import Medium, MediumType from tidy3d.components.monitor import ( + AstigmaticGaussianOverlapMonitor, AuxFieldTimeMonitor, DiffractionMonitor, DirectivityMonitor, @@ -30,6 +31,7 @@ FieldTimeMonitor, FluxMonitor, FluxTimeMonitor, + GaussianOverlapMonitor, MediumMonitor, ModeMonitor, ModeSolverMonitor, @@ -1596,7 +1598,58 @@ class MediumData(MediumDataset, AbstractFieldData): ) -class ModeData(ModeSolverDataset, ElectromagneticFieldData): +class AbstractOverlapData(ElectromagneticFieldData): + amps: ModeAmpsDataArray = pd.Field( + ..., + title="Amplitudes", + description="Complex-valued amplitudes of the overlap decomposition.", + ) + + def normalize(self, source_spectrum_fn) -> AbstractOverlapData: + """Return copy of self after normalization is applied using source spectrum function.""" + if self.amps is None: + return self.copy() + source_freq_amps = source_spectrum_fn(self.amps.f)[None, :, None] + new_amps = (self.amps / source_freq_amps).astype(self.amps.dtype) + return self.copy(update={"amps": new_amps}) + + @property + def time_reversed_copy(self) -> FieldData: + """Make a copy of the data with direction-reversed fields. In lossy or gyrotropic systems, + the time-reversed fields will not be the same as the backward-propagating modes.""" + + # Time reversal + new_data = {} + for comp, field in self.field_components.items(): + if comp[0] == "H": + new_data[comp] = -np.conj(field) + else: + new_data[comp] = np.conj(field) + + # switch direction in the monitor + mnt = self.monitor + new_dir = "+" if mnt.store_fields_direction == "-" else "-" + update_dict = {"store_fields_direction": new_dir} + if hasattr(mnt, "direction"): + update_dict["direction"] = new_dir + new_data["monitor"] = mnt.updated_copy(**update_dict) + return self.copy(update=new_data) + + +class FieldOverlapData(AbstractOverlapData): + monitor: Union[GaussianOverlapMonitor, AstigmaticGaussianOverlapMonitor] = pd.Field( + ..., title="Monitor", description="Monitor associated with the data." + ) + + def _make_adjoint_sources( + self, dataset_names: list[str], fwidth: float + ) -> list[Union[CustomCurrentSource, PointDipole]]: + """Converts a :class:`.FieldData` to a list of adjoint current or point sources.""" + + raise NotImplementedError("Could not formulate adjoint source for overlap monitor output.") + + +class ModeData(ModeSolverDataset, AbstractOverlapData): """ Data associated with a :class:`.ModeMonitor`: modal amplitudes, propagation indices and mode profiles. @@ -1636,11 +1689,7 @@ class ModeData(ModeSolverDataset, ElectromagneticFieldData): """ monitor: ModeMonitor = pd.Field( - ..., title="Monitor", description="Mode monitor associated with the data." - ) - - amps: ModeAmpsDataArray = pd.Field( - ..., title="Amplitudes", description="Complex-valued amplitudes associated with the mode." + ..., title="Monitor", description="Monitor associated with the data." ) eps_spec: list[EpsSpecType] = pd.Field( @@ -1662,12 +1711,6 @@ def eps_spec_match_mode_spec(cls, val, values): ) return val - def normalize(self, source_spectrum_fn) -> ModeData: - """Return copy of self after normalization is applied using source spectrum function.""" - source_freq_amps = source_spectrum_fn(self.amps.f)[None, :, None] - new_amps = (self.amps / source_freq_amps).astype(self.amps.dtype) - return self.copy(update={"amps": new_amps}) - def overlap_sort( self, track_freq: TrackFreq, @@ -1965,25 +2008,6 @@ def _group_index_post_process(self, frequency_step: float) -> ModeData: return self.copy(update=update_dict) - @property - def time_reversed_copy(self) -> FieldData: - """Make a copy of the data with direction-reversed fields. In lossy or gyrotropic systems, - the time-reversed fields will not be the same as the backward-propagating modes.""" - - # Time reversal - new_data = {} - for comp, field in self.field_components.items(): - if comp[0] == "H": - new_data[comp] = -np.conj(field) - else: - new_data[comp] = np.conj(field) - - # switch direction in the monitor - mnt = self.monitor - new_dir = "+" if mnt.store_fields_direction == "-" else "-" - new_data["monitor"] = mnt.updated_copy(store_fields_direction=new_dir) - return self.copy(update=new_data) - def _colocated_propagation_axes_field(self, field_name: Literal["E", "H"]) -> DataArray: """Collect a field DataArray containing all 3 field components and rotate from frame with normal axis along z to frame with propagation axis along z. @@ -2262,29 +2286,6 @@ class ModeSolverData(ModeData): None, title="Amplitudes", description="Unused for ModeSolverData." ) - def normalize(self, source_spectrum_fn: Callable[[float], complex]) -> ModeSolverData: - """Return copy of self after normalization is applied using source spectrum function.""" - return self.copy() - - @property - def time_reversed_copy(self) -> FieldData: - """Make a copy of the data with direction-reversed fields. In lossy or gyrotropic systems, - the time-reversed fields will not be the same as the backward-propagating modes.""" - - # Time reversal - new_data = {} - for comp, field in self.field_components.items(): - if comp[0] == "H": - new_data[comp] = -np.conj(field) - else: - new_data[comp] = np.conj(field) - - # switch direction in the monitor - mnt = self.monitor - new_dir = "+" if mnt.store_fields_direction == "-" else "-" - new_data["monitor"] = mnt.updated_copy(direction=new_dir, store_fields_direction=new_dir) - return self.copy(update=new_data) - def _check_fields_stored(self, components: list[str]): """Check that all requested field components are stored in the data.""" missing_comps = [comp for comp in components if comp not in self.field_components.keys()] diff --git a/tidy3d/components/monitor.py b/tidy3d/components/monitor.py index 64386a5d09..4f453eefee 100644 --- a/tidy3d/components/monitor.py +++ b/tidy3d/components/monitor.py @@ -330,7 +330,79 @@ def normal_axis(self) -> Axis: return self.size.index(0.0) -class AbstractModeMonitor(PlanarMonitor, FreqMonitor): +class AbstractOverlapMonitor(PlanarMonitor, FreqMonitor): + """:class:`Monitor` that projects fields onto a specified basis and stores overlap amplitudes. + + This base is shared by ModeMonitor and Gaussian-overlap monitors. + """ + + store_fields_direction: Direction = pydantic.Field( + None, + title="Store Fields", + description="Propagation direction for the field profiles stored from overlap calculation.", + ) + + colocate: bool = pydantic.Field( + True, + title="Colocate Fields", + description="Toggle whether fields should be colocated to grid cell boundaries (i.e. primal grid nodes).", + ) + + conjugated_dot_product: bool = pydantic.Field( + True, + title="Conjugated Dot Product", + description="Use conjugated or non-conjugated dot product for overlap/decomposition.", + ) + + def plot( + self, + x: Optional[float] = None, + y: Optional[float] = None, + z: Optional[float] = None, + ax: Ax = None, + **patch_kwargs, + ) -> Ax: + """Plot this overlap monitor with an arrow indicating propagation direction.""" + ax = super().plot(x=x, y=y, z=z, ax=ax, **patch_kwargs) + + kwargs_alpha = patch_kwargs.get("alpha") + arrow_alpha = ARROW_ALPHA if kwargs_alpha is None else kwargs_alpha + + ax = self._plot_arrow( + x=x, + y=y, + z=z, + ax=ax, + direction=self._dir_arrow, + bend_radius=None, + bend_axis=None, + color=ARROW_COLOR_MONITOR, + alpha=arrow_alpha, + both_dirs=True, + ) + return ax + + @cached_property + def _dir_arrow(self) -> tuple[float, float, float]: + """Direction vector from angles used for overlap (children must define angles).""" + theta, phi = self._angles + dx = np.cos(phi) * np.sin(theta) + dy = np.sin(phi) * np.sin(theta) + dz = np.cos(theta) + return self.unpop_axis(dz, (dx, dy), axis=self.normal_axis) + + @property + def _angles(self) -> tuple[float, float]: + """Angle tuple (theta, phi) in radians. Children override to supply values.""" + return (0.0, 0.0) + + def _storage_size_solver(self, num_cells: int, tmesh: ArrayFloat1D) -> int: + """Default size of intermediate data; store all fields on plane for overlap.""" + num_sample = len(getattr(self, "freqs", [0])) + return BYTES_COMPLEX * num_cells * num_sample * 6 + + +class AbstractModeMonitor(AbstractOverlapMonitor): """:class:`Monitor` that records mode-related data.""" mode_spec: ModeSpec = pydantic.Field( @@ -393,9 +465,13 @@ def plot( ) return ax + @cached_property + def _angles(self) -> tuple[float, float]: + return (self.mode_spec.angle_theta, self.mode_spec.angle_phi) + @cached_property def _dir_arrow(self) -> tuple[float, float, float]: - """Source direction normal vector in cartesian coordinates.""" + """Monitor direction normal vector in cartesian coordinates.""" dx = np.cos(self.mode_spec.angle_phi) * np.sin(self.mode_spec.angle_theta) dy = np.sin(self.mode_spec.angle_phi) * np.sin(self.mode_spec.angle_theta) dz = np.cos(self.mode_spec.angle_theta) @@ -433,6 +509,135 @@ def _storage_size_solver(self, num_cells: int, tmesh: ArrayFloat1D) -> int: return bytes_single +class AbstractGaussianOverlapMonitor(AbstractOverlapMonitor): + """:class:`Monitor` that records amplitudes from decomposition onto a Gaussian-like beam. + + Common fields and behavior shared by GaussianOverlapMonitor and + AstigmaticGaussianOverlapMonitor. + """ + + angle_theta: float = pydantic.Field( + 0.0, + title="Polar Angle", + description="Polar angle of propagation direction.", + units=RADIAN, + ) + + angle_phi: float = pydantic.Field( + 0.0, + title="Azimuth Angle", + description="Azimuth angle of propagation direction.", + units=RADIAN, + ) + + pol_angle: float = pydantic.Field( + 0, + title="Polarization Angle", + description="Specifies the angle between the electric field polarization of the " + "source and the plane defined by the injection axis and the propagation axis (rad). " + "``pol_angle=0`` (default) specifies P polarization, " + "while ``pol_angle=np.pi/2`` specifies S polarization. " + "At normal incidence when S and P are undefined, ``pol_angle=0`` defines: " + "- ``Ey`` polarization for propagation along ``x``." + "- ``Ex`` polarization for propagation along ``y``." + "- ``Ex`` polarization for propagation along ``z``.", + units=RADIAN, + ) + + @property + def _angles(self) -> tuple[float, float]: + return (self.angle_theta, self.angle_phi) + + def storage_size(self, num_cells: int, tmesh: ArrayFloat1D) -> int: + """Size of monitor storage given the number of points after discretization.""" + # store complex amplitudes for +/- directions + num_dirs = 2 + return BYTES_COMPLEX * len(self.freqs) * num_dirs + + +class GaussianOverlapMonitor(AbstractGaussianOverlapMonitor): + """:class:`Monitor` that records amplitudes from decomposition onto a Gaussian beam. + + Example + ------- + >>> gauss = GaussianOverlapMonitor( + ... size=(0,3,3), + ... source_time=pulse, + ... pol_angle=np.pi / 2, + ... waist_radius=1.0) + + Notes + -------- + If one wants the focus 'in front' of the source, a negative value of ``waist_distance`` + is needed. See also :class:`.GaussianBeam`. + """ + + waist_radius: pydantic.PositiveFloat = pydantic.Field( + 1.0, + title="Waist Radius", + description="Radius of the beam at the waist.", + units=MICROMETER, + ) + + waist_distance: float = pydantic.Field( + 0.0, + title="Waist Distance", + description="Distance from the beam waist along the propagation direction. " + "A positive value means the waist is positioned behind the source, considering the propagation direction. " + "For example, for a beam propagating in the ``+`` direction, a positive value of ``waist_distance`` " + "means the beam waist is positioned in the ``-`` direction (behind the source). " + "A negative value means the beam waist is in the ``+`` direction (in front of the source). " + "For an angled source, the distance is defined along the rotated propagation direction.", + units=MICROMETER, + ) + + +class AstigmaticGaussianOverlapMonitor(AbstractGaussianOverlapMonitor): + """:class:`Monitor` that records amplitudes from decomposition onto an astigmatic Gaussian beam. + + The simple astigmatic Gaussian distribution allows + both an elliptical intensity profile and different waist locations for the two principal axes + of the ellipse. When equal waist sizes and equal waist distances are specified in the two + directions, this monitor becomes equivalent to :class:`GaussianOverlapMonitor`. + + Notes + ----- + + This class implements the simple astigmatic Gaussian beam described in _`[1]`. + + **References**: + + .. [1] Kochkina et al., Applied Optics, vol. 52, issue 24, 2013. + + Example + ------- + >>> gauss = AstigmaticGaussianOverlapMonitor( + ... size=(0,3,3), + ... pol_angle=np.pi / 2, + ... waist_sizes=(1.0, 2.0), + ... waist_distances = (3.0, 4.0)) + """ + + waist_sizes: tuple[pydantic.PositiveFloat, pydantic.PositiveFloat] = pydantic.Field( + (1.0, 1.0), + title="Waist sizes", + description="Size of the beam at the waist in the local x and y directions.", + units=MICROMETER, + ) + + waist_distances: tuple[float, float] = pydantic.Field( + (0.0, 0.0), + title="Waist distances", + description="Distance to the beam waist along the propagation direction " + "for the waist sizes in the local x and y directions. " + "When ``direction`` is ``+`` and ``waist_distances`` are positive, the waist " + "is on the ``-`` side (behind) the source plane. When ``direction`` is ``+`` and " + "``waist_distances`` are negative, the waist is on the ``+`` side (in front) of " + "the source plane.", + units=MICROMETER, + ) + + class FieldMonitor(AbstractFieldMonitor, FreqMonitor): """:class:`Monitor` that records electromagnetic fields in the frequency domain. diff --git a/tidy3d/components/types/monitor.py b/tidy3d/components/types/monitor.py index d8585cab51..527e0f3836 100644 --- a/tidy3d/components/types/monitor.py +++ b/tidy3d/components/types/monitor.py @@ -6,6 +6,7 @@ from tidy3d.components.microwave.monitor import MicrowaveModeMonitor, MicrowaveModeSolverMonitor from tidy3d.components.monitor import ( + AstigmaticGaussianOverlapMonitor, AuxFieldTimeMonitor, DiffractionMonitor, DirectivityMonitor, @@ -16,6 +17,7 @@ FieldTimeMonitor, FluxMonitor, FluxTimeMonitor, + GaussianOverlapMonitor, MediumMonitor, ModeMonitor, ModeSolverMonitor, @@ -40,4 +42,6 @@ DirectivityMonitor, MicrowaveModeMonitor, MicrowaveModeSolverMonitor, + GaussianOverlapMonitor, + AstigmaticGaussianOverlapMonitor, ] diff --git a/tidy3d/components/types/monitor_data.py b/tidy3d/components/types/monitor_data.py index 50f5188865..fb1e6ead4c 100644 --- a/tidy3d/components/types/monitor_data.py +++ b/tidy3d/components/types/monitor_data.py @@ -9,6 +9,7 @@ DiffractionData, DirectivityData, FieldData, + FieldOverlapData, FieldProjectionAngleData, FieldProjectionCartesianData, FieldProjectionKSpaceData, @@ -40,6 +41,7 @@ FieldProjectionAngleData, DiffractionData, DirectivityData, + FieldOverlapData, MicrowaveModeData, MicrowaveModeSolverData, ) diff --git a/tidy3d/plugins/smatrix/__init__.py b/tidy3d/plugins/smatrix/__init__.py index f38f21825e..4167f69905 100644 --- a/tidy3d/plugins/smatrix/__init__.py +++ b/tidy3d/plugins/smatrix/__init__.py @@ -25,7 +25,7 @@ ) from tidy3d.plugins.smatrix.data.types import ComponentModelerDataType from tidy3d.plugins.smatrix.ports.coaxial_lumped import CoaxialLumpedPort -from tidy3d.plugins.smatrix.ports.modal import Port +from tidy3d.plugins.smatrix.ports.modal import AstigmaticGaussianPort, GaussianPort, Port from tidy3d.plugins.smatrix.ports.rectangular_lumped import LumpedPort from tidy3d.plugins.smatrix.ports.wave import WavePort @@ -41,10 +41,12 @@ __all__ = [ "AbstractComponentModeler", + "AstigmaticGaussianPort", "CoaxialLumpedPort", "ComponentModeler", "ComponentModelerDataType", "ComponentModelerType", + "GaussianPort", "LumpedPort", "MicrowaveSMatrixData", "ModalComponentModeler", diff --git a/tidy3d/plugins/smatrix/analysis/modal.py b/tidy3d/plugins/smatrix/analysis/modal.py index 147b472b74..5ff235e6f5 100644 --- a/tidy3d/plugins/smatrix/analysis/modal.py +++ b/tidy3d/plugins/smatrix/analysis/modal.py @@ -14,7 +14,8 @@ def modal_construct_smatrix(modeler_data: ModalComponentModelerData) -> ModalPor """Constructs the S-matrix from the data of a :class:`.ModalComponentModeler`. This function post-processes the :class:`.SimulationData` from a series of - simulations to compute the scattering matrix (S-matrix). + simulations to compute the scattering matrix (S-matrix). Supports both modal + ports (ModeMonitor/ModeData) and Gaussian ports (GaussianOverlapMonitor/FieldOverlapData). Parameters ---------- @@ -65,11 +66,17 @@ def modal_construct_smatrix(modeler_data: ModalComponentModelerData) -> ModalPor port_name_out, mode_index_out = row_index port_out = modeler_data.modeler.get_port_by_name(port_name=port_name_out) - # directly compute the element - mode_amps_data = sim_data[port_out.name].copy().amps + # Read overlap amplitudes from either ModeData or FieldOverlapData. + # Both expose '.amps' over coords ('direction', 'f', 'mode_index'). + overlap_amps = sim_data[port_out.name].copy().amps + + # Outgoing direction at the output port: flip the "input" convention. dir_out = "-" if port_out.direction == "+" else "+" - amp = mode_amps_data.sel(f=coords["f"], direction=dir_out, mode_index=mode_index_out) + amp = overlap_amps.sel(f=coords["f"], direction=dir_out, mode_index=mode_index_out) + + # Normalization provided by modeler (handles both modal and gaussian runs). source_norm = modeler_data.modeler._normalization_factor(port_in, sim_data) + s_matrix_elements = np.array(amp.data) / np.array(source_norm) coords_set = { "port_in": port_name_in, diff --git a/tidy3d/plugins/smatrix/component_modelers/modal.py b/tidy3d/plugins/smatrix/component_modelers/modal.py index 7b2a243ff8..0254173761 100644 --- a/tidy3d/plugins/smatrix/component_modelers/modal.py +++ b/tidy3d/plugins/smatrix/component_modelers/modal.py @@ -10,12 +10,9 @@ from tidy3d.components.base import cached_property from tidy3d.components.data.sim_data import SimulationData from tidy3d.components.index import SimulationMap -from tidy3d.components.monitor import ModeMonitor -from tidy3d.components.source.field import ModeSource -from tidy3d.components.source.time import GaussianPulse from tidy3d.components.types import Ax, Complex from tidy3d.components.viz import add_ax_if_none, equal_aspect -from tidy3d.plugins.smatrix.ports.modal import Port +from tidy3d.plugins.smatrix.ports.modal import ModalPortType, Port from tidy3d.plugins.smatrix.types import Element, MatrixIndex from .base import FWIDTH_FRAC, AbstractComponentModeler @@ -26,7 +23,7 @@ class ModalComponentModeler(AbstractComponentModeler): This class orchestrates the process of running multiple simulations to derive the scattering matrix (S-matrix) of a component. It uses modal - sources and monitors defined by a set of ports. + or Gaussian sources and monitors defined by a set of ports. See Also -------- @@ -34,7 +31,7 @@ class ModalComponentModeler(AbstractComponentModeler): * `Computing the scattering matrix of a device <../../notebooks/SMatrix.html>`_ """ - ports: tuple[Port, ...] = pd.Field( + ports: tuple[ModalPortType, ...] = pd.Field( (), title="Ports", description="Collection of ports describing the scattering matrix elements. " @@ -100,7 +97,7 @@ def matrix_indices_monitor(self) -> tuple[MatrixIndex, ...]: """ matrix_indices = [] for port in self.ports: - for mode_index in range(port.mode_spec.num_modes): + for mode_index in range(port.num_modes): matrix_indices.append((port.name, mode_index)) return tuple(matrix_indices) @@ -139,65 +136,18 @@ def get_port_names(matrix_elements: tuple[str, int]) -> list[str]: return port_names_out, port_names_in - def to_monitor(self, port: Port) -> ModeMonitor: - """Creates a mode monitor from a given port. + def to_monitor(self, port: Port): + """Creates an overlap monitor from a given port (modal or gaussian).""" + return port.to_monitor(freqs=self.freqs) - This monitor is used to measure the mode amplitudes at the port. - - Parameters - ---------- - port : Port - The port to convert into a monitor. - - Returns - ------- - ModeMonitor - A :class:`.ModeMonitor` configured to match the port's - properties. - """ - return ModeMonitor( - center=port.center, - size=port.size, - freqs=self.freqs, - mode_spec=port.mode_spec, - name=port.name, - ) - - def to_source( - self, port: Port, mode_index: int, num_freqs: int = 1, **kwargs - ) -> list[ModeSource]: - """Creates a mode source from a given port. - - This source is used to excite a specific mode at the port. - - Parameters - ---------- - port : Port - The port to convert into a source. - mode_index : int - The index of the mode to excite. - num_freqs : int, optional - The number of frequency points for the source, by default 1. - - Returns - ------- - List[ModeSource] - A list containing a single :class:`.ModeSource` configured to - excite the specified mode at the port. - """ - freq0 = np.mean(self.freqs) - fdiff = max(self.freqs) - min(self.freqs) + def to_source(self, port: Port, mode_index: int, num_freqs: int = 1, **kwargs): + """Creates a source from a given port (modal or gaussian).""" + freqs = self.freqs + freq0 = 0.5 * (max(freqs) + min(freqs)) + fdiff = max(freqs) - min(freqs) fwidth = max(fdiff, freq0 * FWIDTH_FRAC) - return ModeSource( - center=port.center, - size=port.size, - source_time=GaussianPulse(freq0=freq0, fwidth=fwidth), - mode_spec=port.mode_spec, - mode_index=mode_index, - direction=port.direction, - name=port.name, - num_freqs=num_freqs, - **kwargs, + return port.to_source( + freq0=freq0, fwidth=fwidth, mode_index=mode_index, num_freqs=num_freqs ) def shift_port(self, port: Port) -> Port: @@ -256,8 +206,9 @@ def plot_sim( plot_sources = [] for port_source in self.ports: - mode_source_0 = self.to_source(port=port_source, mode_index=0) - plot_sources.append(mode_source_0) + # for plotting, use mode_index=0 (gaussian ignores it) + src0 = self.to_source(port=port_source, mode_index=0) + plot_sources.append(src0) sim_plot = self.simulation.copy(update={"sources": plot_sources}) return sim_plot.plot(x=x, y=y, z=z, ax=ax) @@ -297,8 +248,8 @@ def plot_sim_eps( plot_sources = [] for port_source in self.ports: - mode_source_0 = self.to_source(port=port_source, mode_index=0) - plot_sources.append(mode_source_0) + src0 = self.to_source(port=port_source, mode_index=0) + plot_sources.append(src0) sim_plot = self.simulation.copy(update={"sources": plot_sources}) return sim_plot.plot_eps(x=x, y=y, z=z, ax=ax, **kwargs) @@ -321,8 +272,9 @@ def _normalization_factor(self, port_source: Port, sim_data: SimulationData) -> """ port_monitor_data = sim_data[port_source.name] - mode_index = sim_data.simulation.sources[0].mode_index - + # some sources (GaussianBeam) don't have 'mode_index'; default to 0 + src = sim_data.simulation.sources[0] + mode_index = getattr(src, "mode_index", 0) normalize_amps = port_monitor_data.amps.sel( f=np.array(self.freqs), direction=port_source.direction, diff --git a/tidy3d/plugins/smatrix/ports/modal.py b/tidy3d/plugins/smatrix/ports/modal.py index 38651ab364..a46cce2c8f 100644 --- a/tidy3d/plugins/smatrix/ports/modal.py +++ b/tidy3d/plugins/smatrix/ports/modal.py @@ -2,12 +2,23 @@ from __future__ import annotations +from abc import ABC +from typing import Union + import pydantic.v1 as pd from tidy3d.components.data.data_array import DataArray from tidy3d.components.geometry.base import Box from tidy3d.components.mode_spec import ModeSpec +from tidy3d.components.monitor import ( + AstigmaticGaussianOverlapMonitor, + GaussianOverlapMonitor, + ModeMonitor, +) +from tidy3d.components.source.field import AstigmaticGaussianBeam, GaussianBeam, ModeSource +from tidy3d.components.source.time import GaussianPulse from tidy3d.components.types import Direction +from tidy3d.constants import MICROMETER, RADIAN class ModalPortDataArray(DataArray): @@ -36,26 +47,203 @@ class ModalPortDataArray(DataArray): _data_attrs = {"long_name": "modal port matrix element"} -class Port(Box): - """Specifies a port for S-matrix calculation. - - A port defines a location and a set of modes for which the S-matrix - is calculated. - """ +class AbstractPort(Box, ABC): + """Abstract base class for modal/Gaussian ports for S-matrix calculation.""" direction: Direction = pd.Field( ..., title="Direction", description="'+' or '-', defining which direction is considered 'input'.", ) - mode_spec: ModeSpec = pd.Field( - ModeSpec(), - title="Mode Specification", - description="Specifies how the mode solver will solve for the modes of the port.", - ) name: str = pd.Field( ..., title="Name", description="Unique name for the port.", min_length=1, ) + + @property + def num_modes(self) -> int: + """Number of modes defined on this port.""" + return 1 + + +class Port(AbstractPort): + """Specifies a modal port for S-matrix calculation. + + A port defines a location and a set of modes for which the S-matrix + is calculated. + """ + + # restore mode_spec for modal ports + mode_spec: ModeSpec = pd.Field( + ModeSpec(), + title="Mode Specification", + description="Specifies how the mode solver will solve for the modes of the port.", + ) + + # factories + def to_monitor(self, freqs: tuple[float, ...]) -> ModeMonitor: + """Create a ModeMonitor matching this modal port.""" + return ModeMonitor( + center=self.center, + size=self.size, + freqs=freqs, + mode_spec=self.mode_spec, + name=self.name, + ) + + def to_source( + self, freq0: float, fwidth: float, mode_index: int, num_freqs: int = 1 + ) -> ModeSource: + """Create a ModeSource matching this modal port.""" + return ModeSource( + center=self.center, + size=self.size, + source_time=GaussianPulse(freq0=freq0, fwidth=fwidth), + mode_spec=self.mode_spec, + mode_index=mode_index, + direction=self.direction, + name=self.name, + num_freqs=num_freqs, + ) + + @property + def num_modes(self) -> int: + """Number of modes defined on this port.""" + return self.mode_spec.num_modes + + +class AbstractGaussianPort(AbstractPort, ABC): + """Abstract base for Gaussian-like ports (Gaussian and AstigmaticGaussian).""" + + angle_theta: float = pd.Field( + 0.0, + title="Polar Angle", + description="Polar angle of the propagation axis from the injection axis.", + units=RADIAN, + ) + angle_phi: float = pd.Field( + 0.0, + title="Azimuth Angle", + description="Azimuth angle of the propagation axis in the plane orthogonal to the injection axis.", + units=RADIAN, + ) + pol_angle: float = pd.Field( + 0.0, + title="Polarization Angle", + description="Angle between E-field polarization and the plane defined by the injection axis and propagation axis. " + "0 => P polarization, pi/2 => S polarization.", + units=RADIAN, + ) + + +class GaussianPort(AbstractGaussianPort): + """Specifies a Gaussian port for S-matrix calculation. + + Parameters mirror GaussianBeam and GaussianOverlapMonitor directly. + """ + + # Gaussian beam waist parameters (same as GaussianBeam/GaussianOverlapMonitor) + waist_radius: float = pd.Field( + 1.0, + title="Waist Radius", + description="Radius of the beam at the waist.", + units=MICROMETER, + ) + waist_distance: float = pd.Field( + 0.0, + title="Waist Distance", + description="Signed distance from the beam waist along the propagation direction. " + "Positive means waist behind the source for '+' direction; negative means in front.", + units=MICROMETER, + ) + + # factories + def to_monitor(self, freqs: tuple[float, ...]) -> GaussianOverlapMonitor: + """Create a GaussianOverlapMonitor matching this gaussian port.""" + return GaussianOverlapMonitor( + center=self.center, + size=self.size, + freqs=freqs, + angle_theta=self.angle_theta, + angle_phi=self.angle_phi, + pol_angle=self.pol_angle, + waist_radius=self.waist_radius, + waist_distance=self.waist_distance, + name=self.name, + ) + + def to_source( + self, freq0: float, fwidth: float, mode_index: int = 0, num_freqs: int = 1 + ) -> GaussianBeam: + """Create a GaussianBeam matching this gaussian port. Ignores mode_index.""" + return GaussianBeam( + center=self.center, + size=self.size, + source_time=GaussianPulse(freq0=freq0, fwidth=fwidth), + angle_theta=self.angle_theta, + angle_phi=self.angle_phi, + pol_angle=self.pol_angle, + waist_radius=self.waist_radius, + waist_distance=self.waist_distance, + direction=self.direction, + name=self.name, + num_freqs=num_freqs, + ) + + +class AstigmaticGaussianPort(AbstractGaussianPort): + """Specifies an astigmatic Gaussian port for S-matrix calculation. + + Parameters mirror AstigmaticGaussianBeam and AstigmaticGaussianOverlapMonitor directly. + """ + + waist_sizes: tuple[pd.PositiveFloat, pd.PositiveFloat] = pd.Field( + (1.0, 1.0), + title="Waist sizes", + description="Size of the beam at the waist in the local x and y directions.", + units=MICROMETER, + ) + waist_distances: tuple[float, float] = pd.Field( + (0.0, 0.0), + title="Waist distances", + description="Distance to the beam waist along the propagation direction for the waist sizes in the local x and y directions.", + units=MICROMETER, + ) + + # factories + def to_monitor(self, freqs: tuple[float, ...]) -> AstigmaticGaussianOverlapMonitor: + """Create an AstigmaticGaussianOverlapMonitor matching this port.""" + return AstigmaticGaussianOverlapMonitor( + center=self.center, + size=self.size, + freqs=freqs, + angle_theta=self.angle_theta, + angle_phi=self.angle_phi, + pol_angle=self.pol_angle, + waist_sizes=self.waist_sizes, + waist_distances=self.waist_distances, + name=self.name, + ) + + def to_source( + self, freq0: float, fwidth: float, mode_index: int = 0, num_freqs: int = 1 + ) -> AstigmaticGaussianBeam: + """Create an AstigmaticGaussianBeam matching this port. Ignores mode_index.""" + return AstigmaticGaussianBeam( + center=self.center, + size=self.size, + source_time=GaussianPulse(freq0=freq0, fwidth=fwidth), + angle_theta=self.angle_theta, + angle_phi=self.angle_phi, + pol_angle=self.pol_angle, + waist_sizes=self.waist_sizes, + waist_distances=self.waist_distances, + direction=self.direction, + name=self.name, + num_freqs=num_freqs, + ) + + +ModalPortType = Union[Port, GaussianPort, AstigmaticGaussianPort]