Skip to content

Commit c30ee71

Browse files
committed
Add local subpixel integration
1 parent ffca061 commit c30ee71

File tree

7 files changed

+117
-12
lines changed

7 files changed

+117
-12
lines changed

tests/conftest.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from autograd.wrap_util import unary_to_nary
1414

1515
import tidy3d as td
16+
from tidy3d.config import config
1617
from tidy3d.log import DEFAULT_LEVEL, set_logging_console, set_logging_level
1718

1819

@@ -94,6 +95,15 @@ def mpl_config_interactive():
9495
mpl.use(original_backend)
9596

9697

98+
@pytest.fixture(autouse=True)
99+
def disable_local_subpixel():
100+
"""Disable local subpixel for the unit tests."""
101+
use_local_subpixel = config.use_local_subpixel
102+
config.use_local_subpixel = False
103+
yield
104+
config.use_local_subpixel = use_local_subpixel
105+
106+
97107
@pytest.fixture
98108
def dir_name(request):
99109
return request.param

tests/test_components/test_packaging.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22

33
import pytest
44

5-
from tidy3d.packaging import Tidy3dImportError, check_import, verify_packages_import
5+
from tidy3d.packaging import (
6+
Tidy3dImportError,
7+
check_import,
8+
supports_local_subpixel,
9+
tidy3d_extras,
10+
verify_packages_import,
11+
)
612

713
assert check_import("tidy3d") is True
814

@@ -65,5 +71,19 @@ def test_check_import():
6571
assert mock_check_import("module2") is False
6672

6773

74+
def test_tidy3d_extras():
75+
import importlib
76+
77+
has_tidy3d_extras = importlib.util.find_spec("tidy3d_extras") is not None
78+
print(f"has_tidy3d_extras = {has_tidy3d_extras}")
79+
80+
@supports_local_subpixel
81+
def get_eps():
82+
assert tidy3d_extras["use_local_subpixel"] is False
83+
assert tidy3d_extras["mod"] is None
84+
85+
get_eps()
86+
87+
6888
if __name__ == "__main__":
6989
pytest.main()

tidy3d/components/mode/mode_solver.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
from tidy3d.constants import C_0
6363
from tidy3d.exceptions import SetupError, ValidationError
6464
from tidy3d.log import log
65+
from tidy3d.packaging import supports_local_subpixel, tidy3d_extras
6566

6667
# Importing the local solver may not work if e.g. scipy is not installed
6768
IMPORT_ERROR_MSG = """Could not import local solver, 'ModeSolver' objects can still be constructed
@@ -316,7 +317,7 @@ def _get_solver_grid(
316317
_, plane_inds = Box.pop_axis([0, 1, 2], normal_axis)
317318
for dim, sym in enumerate(solver_symmetry):
318319
if sym != 0:
319-
span_inds[plane_inds[dim], 0] += np.diff(span_inds[plane_inds[dim]]) // 2
320+
span_inds[plane_inds[dim], 0] += np.diff(span_inds[plane_inds[dim]])[0] // 2
320321

321322
return simulation._subgrid(span_inds=span_inds)
322323

@@ -1355,18 +1356,21 @@ def _diagonal_material_profile_modal_plane_tranform(
13551356

13561357
def _solver_eps(self, freq: float) -> ArrayComplex4D:
13571358
"""Diagonal permittivity in the shape needed by solver, with normal axis rotated to z."""
1358-
13591359
# Get diagonal epsilon components in the plane
13601360
eps_tensor = self._get_epsilon(freq)
13611361
# tranformation
13621362
return self._tensorial_material_profile_modal_plane_tranform(eps_tensor, self.normal_axis)
13631363

1364+
@supports_local_subpixel
13641365
def _solve_all_freqs(
13651366
self,
13661367
coords: tuple[ArrayFloat1D, ArrayFloat1D],
13671368
symmetry: tuple[Symmetry, Symmetry],
13681369
) -> tuple[list[float], list[dict[str, ArrayComplex4D]], list[EpsSpecType]]:
13691370
"""Call the mode solver at all requested frequencies."""
1371+
if tidy3d_extras["use_local_subpixel"]:
1372+
subpixel_ms = tidy3d_extras["mod"].SubpixelModeSolver.from_mode_solver(self)
1373+
return subpixel_ms._solve_all_freqs(coords=coords, symmetry=symmetry)
13701374

13711375
fields = []
13721376
n_complex = []
@@ -1380,13 +1384,19 @@ def _solve_all_freqs(
13801384
eps_spec.append(eps_spec_freq)
13811385
return n_complex, fields, eps_spec
13821386

1387+
@supports_local_subpixel
13831388
def _solve_all_freqs_relative(
13841389
self,
13851390
coords: tuple[ArrayFloat1D, ArrayFloat1D],
13861391
symmetry: tuple[Symmetry, Symmetry],
13871392
basis_fields: list[dict[str, ArrayComplex4D]],
13881393
) -> tuple[list[float], list[dict[str, ArrayComplex4D]], list[EpsSpecType]]:
13891394
"""Call the mode solver at all requested frequencies."""
1395+
if tidy3d_extras["use_local_subpixel"]:
1396+
subpixel_ms = tidy3d_extras["mod"].SubpixelModeSolver.from_mode_solver(self)
1397+
return subpixel_ms._solve_all_freqs_relative(
1398+
coords=coords, symmetry=symmetry, basis_fields=basis_fields
1399+
)
13901400

13911401
fields = []
13921402
n_complex = []
@@ -1452,22 +1462,26 @@ def _solve_single_freq(
14521462
)
14531463
return n_complex, fields, eps_spec
14541464

1455-
def _rotate_field_coords_inverse(self, field: FIELD) -> FIELD:
1465+
@classmethod
1466+
def _rotate_field_coords_inverse(
1467+
cls, field: FIELD, normal_axis: Axis, plane: MODE_PLANE_TYPE
1468+
) -> FIELD:
14561469
"""Move the propagation axis to the z axis in the array."""
1457-
f_x, f_y, f_z = np.moveaxis(field, source=1 + self.normal_axis, destination=3)
1458-
f_n, f_ts = self.plane.pop_axis((f_x, f_y, f_z), axis=self.normal_axis)
1459-
return np.stack(self.plane.unpop_axis(f_n, f_ts, axis=2), axis=0)
1470+
f_x, f_y, f_z = np.moveaxis(field, source=1 + normal_axis, destination=3)
1471+
f_n, f_ts = plane.pop_axis((f_x, f_y, f_z), axis=normal_axis)
1472+
return np.stack(plane.unpop_axis(f_n, f_ts, axis=2), axis=0)
14601473

1461-
def _postprocess_solver_fields_inverse(self, fields):
1474+
@classmethod
1475+
def _postprocess_solver_fields_inverse(cls, fields, normal_axis: Axis, plane: MODE_PLANE_TYPE):
14621476
"""Convert ``fields`` to ``solver_fields``. Doesn't change gauge."""
14631477
E = [fields[key] for key in ("Ex", "Ey", "Ez")]
14641478
H = [fields[key] for key in ("Hx", "Hy", "Hz")]
14651479

1466-
(Ex, Ey, Ez) = self._rotate_field_coords_inverse(E)
1467-
(Hx, Hy, Hz) = self._rotate_field_coords_inverse(H)
1480+
(Ex, Ey, Ez) = cls._rotate_field_coords_inverse(E, normal_axis=normal_axis, plane=plane)
1481+
(Hx, Hy, Hz) = cls._rotate_field_coords_inverse(H, normal_axis=normal_axis, plane=plane)
14681482

14691483
# apply -1 to H fields if a reflection was involved in the rotation
1470-
if self.normal_axis == 1:
1484+
if normal_axis == 1:
14711485
Hx *= -1
14721486
Hy *= -1
14731487
Hz *= -1
@@ -1489,7 +1503,9 @@ def _solve_single_freq_relative(
14891503
if not LOCAL_SOLVER_IMPORTED:
14901504
raise ImportError(IMPORT_ERROR_MSG)
14911505

1492-
solver_basis_fields = self._postprocess_solver_fields_inverse(basis_fields)
1506+
solver_basis_fields = self._postprocess_solver_fields_inverse(
1507+
fields=basis_fields, normal_axis=self.normal_axis, plane=self.plane
1508+
)
14931509

14941510
solver_fields, n_complex, eps_spec = compute_modes(
14951511
eps_cross=self._solver_eps(freq),

tidy3d/components/mode/simulation.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,9 @@ def run_local(self):
250250
"""Run locally."""
251251
from .data.sim_data import ModeSimulationData
252252

253+
# repeat the calculation every time, in case use_local_subpixel changed
254+
self._invalidate_solver_cache()
255+
253256
modes_raw = self._mode_solver.data_raw
254257
return ModeSimulationData(simulation=self, modes_raw=modes_raw)
255258

tidy3d/components/simulation.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from tidy3d.constants import C_0, SECOND, fp_eps, inf
2323
from tidy3d.exceptions import SetupError, Tidy3dError, Tidy3dImportError, ValidationError
2424
from tidy3d.log import log
25+
from tidy3d.packaging import supports_local_subpixel, tidy3d_extras
2526
from tidy3d.updater import Updater
2627

2728
from .base import cached_property, skip_if_fields_missing
@@ -1461,6 +1462,7 @@ def epsilon(
14611462
sub_grid = self.discretize(box)
14621463
return self.epsilon_on_grid(grid=sub_grid, coord_key=coord_key, freq=freq)
14631464

1465+
@supports_local_subpixel
14641466
def epsilon_on_grid(
14651467
self,
14661468
grid: Grid,
@@ -1505,6 +1507,10 @@ def epsilon_on_grid(
15051507
"Epsilon calculation may be slow."
15061508
)
15071509

1510+
if tidy3d_extras["use_local_subpixel"]:
1511+
subpixel_sim = tidy3d_extras["mod"].SubpixelSimulation.from_simulation(self)
1512+
return subpixel_sim.epsilon_on_grid(grid=grid, coord_key=coord_key, freq=freq)
1513+
15081514
def get_eps(structure: Structure, frequency: float, coords: Coords):
15091515
"""Select the correct epsilon component if field locations are requested."""
15101516
if coord_key[0] != "E":
@@ -1955,6 +1961,10 @@ def subsection(
19551961
# 2) Assemble the full simulation without validation
19561962
return new_sim.updated_copy(structures=new_structures, deep=deep_copy, validate=False)
19571963

1964+
def _invalidate_solver_cache(self) -> None:
1965+
"""Clear cached attributes that become stale when subpixel changes."""
1966+
self._cached_properties.pop("_mode_solver", None)
1967+
19581968

19591969
class Simulation(AbstractYeeGridSimulation):
19601970
"""

tidy3d/config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
from typing import Optional
6+
57
import pydantic.v1 as pd
68

79
from .log import DEFAULT_LEVEL, LogLevel, set_log_suppression, set_logging_level
@@ -35,6 +37,13 @@ class Config:
3537
"for several elements.",
3638
)
3739

40+
use_local_subpixel: Optional[bool] = pd.Field(
41+
None,
42+
title="Whether to use local subpixel averaging. If 'None', local subpixel "
43+
"averaging will be used if 'tidy3d-extras' is installed and not used otherwise. "
44+
"NOTE: This feature is not yet supported.",
45+
)
46+
3847
@pd.validator("logging_level", pre=True, always=True)
3948
def _set_logging_level(cls, val):
4049
"""Set the logging level if logging_level is changed."""

tidy3d/packaging.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import numpy as np
1414

15+
from .config import config
1516
from .exceptions import Tidy3dImportError
1617

1718
vtk = {
@@ -22,6 +23,8 @@
2223
"numpy_to_vtk": None,
2324
}
2425

26+
tidy3d_extras = {"mod": None, "use_local_subpixel": False}
27+
2528

2629
def check_import(module_name: str) -> bool:
2730
"""
@@ -175,3 +178,37 @@ def get_numpy_major_version(module=np):
175178
major_version = int(module_version.split(".")[0])
176179

177180
return major_version
181+
182+
183+
def supports_local_subpixel(fn):
184+
"""When decorating a method, checks that 'tidy3d-extras' is available,
185+
conditioned on 'config.use_local_subpixel'."""
186+
187+
@functools.wraps(fn)
188+
def _fn(*args, **kwargs):
189+
if config.use_local_subpixel is False:
190+
tidy3d_extras["use_local_subpixel"] = False
191+
tidy3d_extras["mod"] = None
192+
else:
193+
# first try to import the module
194+
if tidy3d_extras["mod"] is None:
195+
try:
196+
import tidy3d_extras as tidy3d_extras_mod
197+
198+
tidy3d_extras["mod"] = tidy3d_extras_mod
199+
tidy3d_extras["use_local_subpixel"] = True
200+
except ImportError as exc:
201+
tidy3d_extras["mod"] = None
202+
tidy3d_extras["use_local_subpixel"] = False
203+
if config.use_local_subpixel is True:
204+
raise Tidy3dImportError(
205+
"The package 'tidy3d-extras' is required for this "
206+
"operation when 'config.use_local_subpixel' is 'True'. "
207+
"Please install the 'tidy3d-extras' package using, for "
208+
"example, 'pip install tidy3d-extras'. NOTE: This "
209+
"feature is not yet supported."
210+
) from exc
211+
212+
return fn(*args, **kwargs)
213+
214+
return _fn

0 commit comments

Comments
 (0)