Skip to content

Commit 4a5add0

Browse files
Feat: Add PointDipole.from_angles() (#2623)
* Feat: Add `PointDipole.from_angles()` Class method that returns a list of `PointDipole` objects used to emulate a single dipole polarized in an arbitrary direction. The direction is specificed using a polar and azimuthal angle. * Fix: Pass polarization argument as string + NumPy-style docstring. * Test: Simple testing function for `PointDipole.from_angles()`. * Refc: Rename `PointDipole.from_angles()` to `PointDipole.sources_from_angles()` --------- Co-authored-by: momchil-flex <[email protected]>
1 parent 6770bcb commit 4a5add0

File tree

3 files changed

+120
-0
lines changed

3 files changed

+120
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111
- Add support for `np.unwrap` in `tidy3d.plugins.autograd`.
1212
- Add Nunley variant to germanium material library based on Nunley et al. 2016 data.
13+
- Add `PointDipole.sources_from_angles()` that constructs a list of `PointDipole` objects needed to emulate a dipole oriented at a user-provided set of polar and azimuthal angles.
1314
- Added `priority` parameter to `web.run()` and related functions to allow vGPU users to set task priority (1-10) in the queue.
1415

1516
### Changed

tests/test_components/test_source.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,67 @@ def test_dipole():
148148
_ = td.PointDipole(size=(1, 1, 1), source_time=g, center=(1, 2, 3), polarization="Ex")
149149

150150

151+
def test_dipole_sources_from_angles():
152+
g = td.GaussianPulse(freq0=1e12, fwidth=0.1e12)
153+
154+
with pytest.raises(pydantic.ValidationError):
155+
_ = td.PointDipole.sources_from_angles(
156+
size=(1, 1, 1),
157+
source_time=g,
158+
center=(1, 2, 3),
159+
angle_theta=np.pi / 4,
160+
angle_phi=np.pi / 4,
161+
)
162+
163+
with pytest.raises(ValueError):
164+
_ = td.PointDipole.sources_from_angles(
165+
source_time=g,
166+
angle_theta=np.pi / 4,
167+
angle_phi=np.pi / 4,
168+
component="invalid",
169+
center=(1, 2, 3),
170+
)
171+
172+
assert (
173+
len(
174+
td.PointDipole.sources_from_angles(
175+
source_time=g,
176+
angle_theta=np.pi / 4,
177+
angle_phi=np.pi / 4,
178+
component="electric",
179+
center=(1, 2, 3),
180+
)
181+
)
182+
== 3
183+
)
184+
185+
assert (
186+
len(
187+
td.PointDipole.sources_from_angles(
188+
source_time=g,
189+
angle_theta=np.pi / 4,
190+
angle_phi=np.pi / 2,
191+
component="electric",
192+
center=(1, 2, 3),
193+
)
194+
)
195+
== 2
196+
)
197+
198+
assert (
199+
len(
200+
td.PointDipole.sources_from_angles(
201+
source_time=g,
202+
angle_theta=np.pi / 2,
203+
angle_phi=np.pi / 2,
204+
component="electric",
205+
center=(1, 2, 3),
206+
)
207+
)
208+
== 1
209+
)
210+
211+
151212
def test_FieldSource():
152213
g = td.GaussianPulse(freq0=1e12, fwidth=0.1e12)
153214
mode_spec = td.ModeSpec(num_modes=2)

tidy3d/components/source/current.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
from abc import ABC
6+
from math import cos, isclose, sin
67
from typing import Optional
78

89
import pydantic.v1 as pydantic
@@ -16,6 +17,7 @@
1617
from tidy3d.constants import MICROMETER
1718

1819
from .base import Source
20+
from .time import SourceTimeType
1921

2022

2123
class CurrentSource(Source, ABC):
@@ -107,6 +109,62 @@ class PointDipole(CurrentSource, ReverseInterpolatedSource):
107109
units=MICROMETER,
108110
)
109111

112+
@classmethod
113+
def sources_from_angles(
114+
cls,
115+
source_time: SourceTimeType,
116+
angle_theta: float,
117+
angle_phi: float,
118+
component: Literal["electric", "magnetic"] = "electric",
119+
**kwargs,
120+
) -> list[PointDipole]:
121+
"""Returns a list of `PointDipole` objects used to emulate a single dipole polarized in an arbitrary direction. The direction is specificed using a polar and azimuthal angle.
122+
123+
Parameters
124+
----------
125+
source_time: :class:`.SourceTime`
126+
Specification of the source time-dependence.
127+
angle_theta : float
128+
Polar angle w.r.t. the z-axis.
129+
angle_phi : float
130+
Azimuth angle around the z-axis.
131+
component : Literal["electric", "magnetic"] = "electric"
132+
The type of polarization.
133+
kwargs : dict
134+
Keyword arguments passed to ``PointDipole()``, excluding ``source_time`` and ``polarization``
135+
136+
Returns
137+
-------
138+
list[PointDipole]
139+
A list of ``PointDipole`` objects that emulate a single dipole with an arbitrary direction of polarization.
140+
"""
141+
if not (component == "electric" or component == "magnetic"):
142+
raise ValueError('Component must be "electric" or "magnetic"')
143+
144+
dipoles: list[PointDipole] = []
145+
polarizations = ["Ex", "Ey", "Ez"] if component == "electric" else ["Hx", "Hy", "Hz"]
146+
147+
multipliers = [
148+
sin(angle_theta) * cos(angle_phi),
149+
sin(angle_theta) * sin(angle_phi),
150+
cos(angle_theta),
151+
]
152+
153+
for polarization, mult in zip(polarizations, multipliers):
154+
if not isclose(mult, 0.0, rel_tol=0.0, abs_tol=1e-9):
155+
modulated_source_time = source_time.updated_copy(
156+
amplitude=source_time.amplitude * mult
157+
)
158+
dipoles.append(
159+
cls(
160+
source_time=modulated_source_time,
161+
polarization=polarization,
162+
**kwargs,
163+
)
164+
)
165+
166+
return dipoles
167+
110168

111169
class CustomCurrentSource(ReverseInterpolatedSource):
112170
"""Implements a source corresponding to an input dataset containing ``E`` and ``H`` fields.

0 commit comments

Comments
 (0)