Skip to content

Commit 437582e

Browse files
lucas-flexcomputemomchil-flex
authored andcommitted
Calculate mode dispersion when group index is calculated (flexcompute#1294)
Signed-off-by: Lucas Heitzmann Gabrielli <lucas@flexcompute.com>
1 parent a9004b8 commit 437582e

File tree

6 files changed

+154
-29
lines changed

6 files changed

+154
-29
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
55

66
## [Unreleased]
77

8+
### Added
9+
- `ModeData.dispersion` and `ModeSolverData.dispersion` are calculated together with the group index.
10+
811
## [2.5.0] - 2023-12-13
912

1013
### Added

tests/test_plugins/test_mode_solver.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,7 @@ def test_mode_solver_2D():
556556
@pytest.mark.parametrize("local", [True, False])
557557
@responses.activate
558558
def test_group_index(mock_remote_api, log_capture, local):
559-
"""Test group index calculation"""
559+
"""Test group index and dispersion calculation"""
560560

561561
simulation = td.Simulation(
562562
size=(5, 5, 1),
@@ -595,10 +595,15 @@ def test_group_index(mock_remote_api, log_capture, local):
595595
if local:
596596
assert modes.n_group is None
597597
assert len(log_capture) == 1
598-
assert log_capture[0][0] == 30
599-
assert "ModeSpec" in log_capture[0][1]
598+
assert modes.dispersion is None
599+
assert len(log_capture) == 2
600+
for log_msg in log_capture:
601+
assert log_msg[0] == 30
602+
assert "ModeSpec" in log_msg[1]
600603
_ = modes.n_group
601-
assert len(log_capture) == 1
604+
assert len(log_capture) == 2
605+
_ = modes.dispersion
606+
assert len(log_capture) == 2
602607

603608
# Group index calculated
604609
ms = ModeSolver(
@@ -613,6 +618,10 @@ def test_group_index(mock_remote_api, log_capture, local):
613618
assert (modes.n_group.sel(mode_index=0).values < 4.2).all()
614619
assert (modes.n_group.sel(mode_index=1).values > 3.7).all()
615620
assert (modes.n_group.sel(mode_index=1).values < 4.0).all()
621+
assert (modes.dispersion.sel(mode_index=0).values > 1400).all()
622+
assert (modes.dispersion.sel(mode_index=0).values < 1500).all()
623+
assert (modes.dispersion.sel(mode_index=1).values > -16500).all()
624+
assert (modes.dispersion.sel(mode_index=1).values < -15000).all()
616625

617626

618627
def test_pml_params():

tidy3d/components/data/data_array.py

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@
77
import dask
88
import h5py
99

10-
from ...constants import HERTZ, SECOND, MICROMETER, RADIAN
10+
from ...constants import (
11+
HERTZ,
12+
MICROMETER,
13+
PICOSECOND_PER_NANOMETER_PER_KILOMETER,
14+
RADIAN,
15+
SECOND,
16+
WATT,
17+
)
1118
from ...exceptions import DataError, FileError
1219
from ..types import Bound, Axis
1320

@@ -440,7 +447,7 @@ class FluxDataArray(DataArray):
440447

441448
__slots__ = ()
442449
_dims = ("f",)
443-
_data_attrs = {"units": "W", "long_name": "flux"}
450+
_data_attrs = {"units": WATT, "long_name": "flux"}
444451

445452

446453
class FluxTimeDataArray(DataArray):
@@ -455,7 +462,7 @@ class FluxTimeDataArray(DataArray):
455462

456463
__slots__ = ()
457464
_dims = ("t",)
458-
_data_attrs = {"units": "W", "long_name": "flux"}
465+
_data_attrs = {"units": WATT, "long_name": "flux"}
459466

460467

461468
class ModeAmpsDataArray(DataArray):
@@ -491,6 +498,41 @@ class ModeIndexDataArray(DataArray):
491498
_data_attrs = {"long_name": "Propagation index"}
492499

493500

501+
class GroupIndexDataArray(DataArray):
502+
"""Group index of a mode.
503+
504+
Example
505+
-------
506+
>>> f = [2e14, 3e14]
507+
>>> mode_index = np.arange(4)
508+
>>> coords = dict(f=f, mode_index=mode_index)
509+
>>> data = GroupIndexDataArray((1+1j) * np.random.random((2,4)), coords=coords)
510+
"""
511+
512+
__slots__ = ()
513+
_dims = ("f", "mode_index")
514+
_data_attrs = {"long_name": "Group index"}
515+
516+
517+
class ModeDispersionDataArray(DataArray):
518+
"""Dispersion parameter of a mode.
519+
520+
Example
521+
-------
522+
>>> f = [2e14, 3e14]
523+
>>> mode_index = np.arange(4)
524+
>>> coords = dict(f=f, mode_index=mode_index)
525+
>>> data = ModeDispersionDataArray((1+1j) * np.random.random((2,4)), coords=coords)
526+
"""
527+
528+
__slots__ = ()
529+
_dims = ("f", "mode_index")
530+
_data_attrs = {
531+
"long_name": "Dispersion parameter",
532+
"units": PICOSECOND_PER_NANOMETER_PER_KILOMETER,
533+
}
534+
535+
494536
class FieldProjectionAngleDataArray(DataArray):
495537
"""Far fields in frequency domain as a function of angles theta and phi.
496538
@@ -654,6 +696,8 @@ class IndexedDataArray(DataArray):
654696
FluxTimeDataArray,
655697
ModeAmpsDataArray,
656698
ModeIndexDataArray,
699+
GroupIndexDataArray,
700+
ModeDispersionDataArray,
657701
FieldProjectionAngleDataArray,
658702
FieldProjectionCartesianDataArray,
659703
FieldProjectionKSpaceDataArray,

tidy3d/components/data/dataset.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from .data_array import DataArray
1515
from .data_array import ScalarFieldDataArray, ScalarFieldTimeDataArray, ScalarModeFieldDataArray
16-
from .data_array import ModeIndexDataArray
16+
from .data_array import ModeIndexDataArray, GroupIndexDataArray, ModeDispersionDataArray
1717
from .data_array import TriangleMeshDataArray
1818
from .data_array import TimeDataArray
1919
from .data_array import PointDataArray, IndexedDataArray, CellDataArray, SpatialDataArray
@@ -23,6 +23,7 @@
2323
from ..types import Axis, Bound, ArrayLike, Ax, Coordinate, Literal
2424
from ..types import vtk, requires_vtk
2525
from ...exceptions import DataError, ValidationError, Tidy3dNotImplementedError
26+
from ...constants import PICOSECOND_PER_NANOMETER_PER_KILOMETER
2627
from ...log import log
2728

2829

@@ -354,31 +355,38 @@ class ModeSolverDataset(ElectromagneticFieldDataset):
354355
description="Complex-valued effective propagation constants associated with the mode.",
355356
)
356357

357-
n_group_raw: ModeIndexDataArray = pd.Field(
358+
n_group_raw: GroupIndexDataArray = pd.Field(
358359
None,
359-
alias="n_group",
360+
alias="n_group", # This is for backwards compatibility only when loading old data
360361
title="Group Index",
361362
description="Index associated with group velocity of the mode.",
362363
)
363364

365+
dispersion_raw: ModeDispersionDataArray = pd.Field(
366+
None,
367+
title="Dispersion",
368+
description="Dispersion parameter for the mode.",
369+
units=PICOSECOND_PER_NANOMETER_PER_KILOMETER,
370+
)
371+
364372
@property
365373
def field_components(self) -> Dict[str, DataArray]:
366374
"""Maps the field components to thier associated data."""
367375

368376
return {field: getattr(self, field) for field in ["Ex", "Ey", "Ez", "Hx", "Hy", "Hz"]}
369377

370378
@property
371-
def n_eff(self):
379+
def n_eff(self) -> ModeIndexDataArray:
372380
"""Real part of the propagation index."""
373381
return self.n_complex.real
374382

375383
@property
376-
def k_eff(self):
384+
def k_eff(self) -> ModeIndexDataArray:
377385
"""Imaginary part of the propagation index."""
378386
return self.n_complex.imag
379387

380388
@property
381-
def n_group(self):
389+
def n_group(self) -> GroupIndexDataArray:
382390
"""Group index."""
383391
if self.n_group_raw is None:
384392
log.warning(
@@ -388,6 +396,22 @@ def n_group(self):
388396
)
389397
return self.n_group_raw
390398

399+
@property
400+
def dispersion(self) -> ModeDispersionDataArray:
401+
r"""Dispersion parameter.
402+
403+
.. math::
404+
405+
D = -\frac{\lambda}{c_0} \frac{{\rm d}^2 n_{\text{eff}}}{{\rm d}\lambda^2}
406+
"""
407+
if self.dispersion_raw is None:
408+
log.warning(
409+
"The dispersion was not computed. To calculate dispersion, pass "
410+
"'group_index_step = True' in the 'ModeSpec'.",
411+
log_once=True,
412+
)
413+
return self.dispersion_raw
414+
391415
def plot_field(self, *args, **kwargs):
392416
"""Warn user to use the :class:`.ModeSolver` ``plot_field`` function now."""
393417
raise DeprecationWarning(

tidy3d/components/data/monitor_data.py

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
from pandas import DataFrame
1212

1313
from .data_array import FluxTimeDataArray, FluxDataArray
14-
from .data_array import MixedModeDataArray, ModeIndexDataArray, ModeAmpsDataArray
14+
from .data_array import MixedModeDataArray, ModeAmpsDataArray
15+
from .data_array import ModeIndexDataArray, GroupIndexDataArray, ModeDispersionDataArray
1516
from .data_array import FieldProjectionAngleDataArray, FieldProjectionCartesianDataArray
1617
from .data_array import FieldProjectionKSpaceDataArray
1718
from .data_array import DataArray, DiffractionDataArray
@@ -32,7 +33,7 @@
3233
from ..source import SourceTimeType, CustomFieldSource
3334
from ..medium import Medium, MediumType
3435
from ...exceptions import SetupError, DataError, Tidy3dNotImplementedError, ValidationError
35-
from ...constants import ETA_0, C_0, MICROMETER
36+
from ...constants import ETA_0, C_0, MICROMETER, PICOSECOND_PER_NANOMETER_PER_KILOMETER
3637
from ...log import log
3738

3839
from ..base_sim.data.monitor_data import AbstractMonitorData
@@ -1160,24 +1161,44 @@ def _group_index_post_process(self, frequency_step: float) -> ModeSolverData:
11601161
back = slice(0, num_freqs, 3)
11611162
center = slice(1, num_freqs, 3)
11621163
fwd = slice(2, num_freqs, 3)
1164+
freqs = freqs[center]
11631165

11641166
# calculate group index
11651167
n_center = self.n_eff.isel(f=center).values
11661168
n_backward = self.n_eff.isel(f=back).values
11671169
n_forward = self.n_eff.isel(f=fwd).values
11681170

1169-
n_group_data = n_center + (n_forward - n_backward) / (2 * frequency_step)
1170-
n_group = ModeIndexDataArray(
1171+
inv_step = 1 / frequency_step
1172+
# n_g = n + f * df/dn
1173+
# dn/df = (n+ - n-) / (2 f df)
1174+
n_group_data = n_center + (n_forward - n_backward) * inv_step * 0.5
1175+
# D = -2 * pi * c / lda^2 * d(v_g^-1)/dw = -(f / c)^2 * (2 * dn/df + f * d2n/df2)
1176+
# d2n/df2 = (n+ - 2n + n-) / (f df)^2
1177+
# The '1e18' factor converts from s/um^2 to ps/(nm km)
1178+
dispersion_data = (
1179+
(n_forward * (inv_step + 1) + n_backward * (inv_step - 1) - n_center * inv_step * 2)
1180+
* freqs.reshape((-1, 1))
1181+
* (-1e18 * inv_step / C_0**2)
1182+
)
1183+
1184+
mode_index = list(self.n_complex.coords["mode_index"].values)
1185+
f = list(freqs)
1186+
n_group = GroupIndexDataArray(
11711187
n_group_data,
1172-
coords={
1173-
"f": list(freqs[center]),
1174-
"mode_index": list(self.n_complex.coords["mode_index"].values),
1175-
},
1176-
attrs={"long name": "Group index"},
1188+
coords={"f": f, "mode_index": mode_index},
1189+
)
1190+
1191+
dispersion = ModeDispersionDataArray(
1192+
dispersion_data,
1193+
coords={"f": f, "mode_index": mode_index},
11771194
)
11781195

11791196
# remove data corresponding to frequencies used only for group index calculation
1180-
update_dict = {"n_complex": self.n_complex.isel(f=center), "n_group_raw": n_group}
1197+
update_dict = {
1198+
"n_complex": self.n_complex.isel(f=center),
1199+
"n_group_raw": n_group,
1200+
"dispersion_raw": dispersion,
1201+
}
11811202

11821203
for key, field in self.field_components.items():
11831204
update_dict[key] = field.isel(f=center)
@@ -1188,7 +1209,7 @@ def _group_index_post_process(self, frequency_step: float) -> ModeSolverData:
11881209
if self.eps_spec:
11891210
update_dict["eps_spec"] = self.eps_spec[center]
11901211

1191-
update_dict["monitor"] = self.monitor.updated_copy(freqs=freqs[center])
1212+
update_dict["monitor"] = self.monitor.updated_copy(freqs=freqs)
11921213

11931214
return self.copy(update=update_dict)
11941215

@@ -1392,25 +1413,32 @@ class ModeData(MonitorData):
13921413
description="Complex-valued effective propagation constants associated with the mode.",
13931414
)
13941415

1395-
n_group_raw: ModeIndexDataArray = pd.Field(
1416+
n_group_raw: GroupIndexDataArray = pd.Field(
13961417
None,
1397-
alias="n_group",
1418+
alias="n_group", # This is for backwards compatibility only when loading old data
13981419
title="Group Index",
13991420
description="Index associated with group velocity of the mode.",
14001421
)
14011422

1423+
dispersion_raw: ModeDispersionDataArray = pd.Field(
1424+
None,
1425+
title="Dispersion",
1426+
description="Dispersion parameter for the mode.",
1427+
units=PICOSECOND_PER_NANOMETER_PER_KILOMETER,
1428+
)
1429+
14021430
@property
1403-
def n_eff(self):
1431+
def n_eff(self) -> ModeIndexDataArray:
14041432
"""Real part of the propagation index."""
14051433
return self.n_complex.real
14061434

14071435
@property
1408-
def k_eff(self):
1436+
def k_eff(self) -> ModeIndexDataArray:
14091437
"""Imaginary part of the propagation index."""
14101438
return self.n_complex.imag
14111439

14121440
@property
1413-
def n_group(self):
1441+
def n_group(self) -> GroupIndexDataArray:
14141442
"""Group index."""
14151443
if self.n_group_raw is None:
14161444
log.warning(
@@ -1420,6 +1448,22 @@ def n_group(self):
14201448
)
14211449
return self.n_group_raw
14221450

1451+
@property
1452+
def dispersion(self) -> ModeDispersionDataArray:
1453+
r"""Dispersion parameter.
1454+
1455+
.. math::
1456+
1457+
D = -\frac{\lambda}{c_0} \frac{{\rm d}^2 n_{\text{eff}}}{{\rm d}\lambda^2}
1458+
"""
1459+
if self.dispersion_raw is None:
1460+
log.warning(
1461+
"The dispersion was not computed. To calculate dispersion, pass "
1462+
"'group_index_step = True' in the 'ModeSpec'.",
1463+
log_once=True,
1464+
)
1465+
return self.dispersion_raw
1466+
14231467
def normalize(self, source_spectrum_fn) -> ModeData:
14241468
"""Return copy of self after normalization is applied using source spectrum function."""
14251469
source_freq_amps = source_spectrum_fn(self.amps.f)[None, :, None]

tidy3d/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
PERCMCUBE = "1/cm^3"
5050
WATT = "W"
5151
VOLT = "V"
52+
PICOSECOND_PER_NANOMETER_PER_KILOMETER = "ps/(nm km)"
5253

5354
THERMAL_CONDUCTIVITY = "W/(um*K)"
5455
SPECIFIC_HEAT_CAPACITY = "J/(kg*K)"

0 commit comments

Comments
 (0)