Skip to content

Commit

Permalink
lumped elements still default to distributed implementation, but now …
Browse files Browse the repository at this point in the history
…allow for restricting the network portion

of the lumped element. PEC connections are used to connect to the bounds of the element.

adding test coverage and fixing some corner cases

fixed small possible bug when performing subdivision

fixing docs and adding pic
  • Loading branch information
dmarek-flex committed Jan 28, 2025
1 parent 3de55ee commit dd4cca8
Show file tree
Hide file tree
Showing 16 changed files with 622 additions and 54 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Bandgap narrowing models for `SemiconductorMedium`: `SlotboomBandGapNarrowing`
- Generation-recombination models for `SemiconductorMedium`: `ShockleyReedHallRecombination`, `RadiativeRecombination`, `AugerRecombination`
- Accessors and length functions implemented for `Result` class in design plugin.
- Advanced option `dist_type` that allows `LinearLumpedElement` to be distributed across grid cells in different ways. For example, restricting the network portion to a single cell and using PEC wires to connect to the desired location of the terminals.

### Changed
- `ModeMonitor` and `ModeSolverMonitor` now use the default `td.ModeSpec()` with `num_modes=1` when `mode_spec` is not provided.
Expand Down
Binary file added docs/_static/img/lumped_dist_type.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/api/lumped_elements.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.. currentmodule:: tidy3d

Lumped elements
Lumped Elements
===============

Passive elements
Expand Down
49 changes: 47 additions & 2 deletions tests/test_components/test_lumped_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,6 @@ def test_RLC_and_lumped_network_agreement(Rval, Lval, Cval, topology):
)
# Check conversion to geometry and to structure
_ = linear_element.to_geometry()
_ = linear_element.to_structure()

sf = linear_element._admittance_transfer_function_scaling()
med_RLC = RLC._to_medium(sf)
Expand All @@ -207,7 +206,6 @@ def test_RLC_and_lumped_network_agreement(Rval, Lval, Cval, topology):
network=network,
)
_ = linear_element.to_geometry()
_ = linear_element.to_structure()
assert np.allclose(med_RLC.eps_model(freqs), med_network.eps_model(freqs), rtol=rtol)


Expand Down Expand Up @@ -262,3 +260,50 @@ def test_impedance_admittance_calculation():

Y_calc = linear_element.admittance(freqs)
assert np.allclose(Y_expected, Y_calc)


@pytest.mark.parametrize("dist_type", ["off", "laterally_only", "on"])
@pytest.mark.parametrize("width", [0, 5])
def test_distribution_variants(dist_type, width):
mm = 1e3
RLC = td.RLCNetwork(
resistance=50,
)

linear_element = td.LinearLumpedElement(
center=[0.5 * mm, 0, 0],
size=[1 * mm, 0, width * mm],
voltage_axis=0,
network=RLC,
name="RLC",
dist_type=dist_type,
)
x = np.linspace(0, 10 * mm, 25)
y = np.linspace(-50 * mm, 50 * mm, 200)
z = np.linspace(-20 * mm, 20 * mm, 100)
coords = td.Coords(x=x, y=y, z=z)
grid = td.Grid(boundaries=coords)

# Grid is coarse enough that there is only one connection made along x
geometry = linear_element.to_geometry(grid)
structure = linear_element.to_PEC_connection(grid)
if dist_type != "on":
assert len(structure.geometry.geometries) == 1
else:
assert structure is None
network = linear_element.to_structure(grid)
L, C = linear_element.estimate_parasitic_elements(grid)
assert C >= 0

# Grid is fine enough that there are two connections made along x
x = np.linspace(0, 10 * mm, 100)
grid = grid.updated_copy(path="boundaries", x=x)
geometry = linear_element.to_geometry(grid)
structure = linear_element.to_PEC_connection(grid)
if dist_type != "on":
assert len(structure.geometry.geometries) == 2
else:
assert structure is None
network = linear_element.to_structure(grid)
L, C = linear_element.estimate_parasitic_elements(grid)
assert C >= 0
51 changes: 51 additions & 0 deletions tests/test_components/test_microwave.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Tests microwave tools."""

from math import isclose

import numpy as np
from tidy3d.components.microwave.formulas.circuit_parameters import (
capacitance_colinear_cylindrical_wire_segments,
capacitance_rectangular_sheets,
inductance_straight_rectangular_wire,
mutual_inductance_colinear_wire_segments,
total_inductance_colinear_rectangular_wire_segments,
)
from tidy3d.constants import EPSILON_0


def test_inductance_formulas():
"""Run the formulas for inductance and compare to precomputed results."""
bar_size = (1000e4, 1e4, 1e4) # case from reference
L1 = inductance_straight_rectangular_wire(bar_size, 0)
assert isclose(L1, 14.816e-6, rel_tol=1e-4)
length = 1e3
L2 = mutual_inductance_colinear_wire_segments(length, length, length / 10)
assert isclose(L2, 0.11181e-9, rel_tol=1e-4)
side = length / 10
L3 = total_inductance_colinear_rectangular_wire_segments(
(side, length, side), (side, length, side), length / 10, 1
)
assert isclose(L3, 1.3625e-9, rel_tol=1e-4)


def test_capacitance_formulas():
"""Run the formulas for capacitance and compare to precomputed results."""
width = 3e3
length = 1e3
d = length / 4.5 # case from reference
C1 = capacitance_rectangular_sheets(width, length, d)
result = 2.347 * EPSILON_0 * width # from reference
assert isclose(C1, result, rel_tol=1e-3)

# case from reference
radius = 0.1e-3
C2 = capacitance_colinear_cylindrical_wire_segments(radius, length, length / 5)
D2 = 0.345
C_ref = np.pi * EPSILON_0 * length / (np.log(length / radius) - 2.303 * D2)
assert isclose(C2, C_ref, rel_tol=1e-3)

# case from reference
C3 = capacitance_colinear_cylindrical_wire_segments(radius, length, length * 5)
D2 = 0.144
C_ref = np.pi * EPSILON_0 * length / (np.log(length / radius) - 2.303 * D2)
assert isclose(C3, C_ref, rel_tol=1e-2)
59 changes: 41 additions & 18 deletions tidy3d/components/geometry/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from ..base import Tidy3dBaseModel
from ..geometry.base import Box
from ..grid.grid import Grid
from ..types import ArrayFloat2D, Axis, MatrixReal4x4, PlanePosition, Shapely
from ..types import ArrayFloat2D, Axis, Coordinate, MatrixReal4x4, PlanePosition, Shapely
from . import base, mesh, polyslab, primitives

GeometryType = Union[
Expand Down Expand Up @@ -291,28 +291,29 @@ class SnappingSpec(Tidy3dBaseModel):
)


def get_closest_value(test: float, coords: np.ArrayLike, upper_bound_idx: int) -> float:
"""Helper to choose the closest value in an array to a given test value,
using the index of the upper bound. The ``upper_bound_idx`` corresponds to the first value in
the ``coords`` array which is greater than or equal to the test value.
"""
# Handle corner cases first
if upper_bound_idx == 0:
return coords[upper_bound_idx]
if upper_bound_idx == len(coords):
return coords[upper_bound_idx - 1]
# General case
lower_bound = coords[upper_bound_idx - 1]
upper_bound = coords[upper_bound_idx]
dlower = abs(test - lower_bound)
dupper = abs(test - upper_bound)
return lower_bound if dlower < dupper else upper_bound


def snap_box_to_grid(grid: Grid, box: Box, snap_spec: SnappingSpec, rtol=fp_eps) -> Box:
"""Snaps a :class:`.Box` to the grid, so that the boundaries of the box are aligned with grid centers or boundaries.
The way in which each dimension of the `box` is snapped to the grid is controlled by ``snap_spec``.
"""

def get_closest_value(test: float, coords: np.ArrayLike, upper_bound_idx: int) -> float:
"""Helper to choose the closest value in an array to a given test value,
using the index of the upper bound. The ``upper_bound_idx`` corresponds to the first value in
the ``coords`` array which is greater than or equal to the test value.
"""
# Handle corner cases first
if upper_bound_idx == 0:
return coords[upper_bound_idx]
if upper_bound_idx == len(coords):
return coords[upper_bound_idx - 1]
# General case
lower_bound = coords[upper_bound_idx - 1]
upper_bound = coords[upper_bound_idx]
dlower = abs(test - lower_bound)
dupper = abs(test - upper_bound)
return lower_bound if dlower < dupper else upper_bound

def get_lower_bound(
test: float, coords: np.ArrayLike, upper_bound_idx: int, rel_tol: float
) -> float:
Expand Down Expand Up @@ -380,3 +381,25 @@ def find_snapping_locations(
min_b[axis] = new_min
max_b[axis] = new_max
return Box.from_bounds(min_b, max_b)


def snap_point_to_grid(
grid: Grid, point: Coordinate, snap_location: tuple[SnapLocation, SnapLocation, SnapLocation]
) -> Coordinate:
"""Snaps a :class:`.Coordinate` to the grid, so that it is coincident with grid centers or boundaries.
The way in which each dimension of the ``point`` is snapped to the grid is controlled by ``snap_location``.
"""
grid_bounds = grid.boundaries.to_list
grid_centers = grid.centers.to_list
snapped_point = 3 * [0]
for axis in range(3):
if snap_location[axis] == SnapLocation.Boundary:
snap_coords = np.array(grid_bounds[axis])
elif snap_location[axis] == SnapLocation.Center:
snap_coords = np.array(grid_centers[axis])

# Locate the interval that includes the test point
min_upper_bound_idx = np.searchsorted(snap_coords, point[axis], side="left")
snapped_point[axis] = get_closest_value(point[axis], snap_coords, min_upper_bound_idx)

return tuple(snapped_point)
9 changes: 6 additions & 3 deletions tidy3d/components/geometry/utils_2d.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"""Utilities for 2D geometry manipulation."""

from math import isclose
from typing import List, Tuple

import numpy as np
import shapely

from ...constants import inf
from ...constants import fp_eps, inf
from ..geometry.base import Box, ClipOperation, Geometry
from ..geometry.polyslab import _MIN_POLYGON_AREA, PolySlab
from ..grid.grid import Grid
Expand Down Expand Up @@ -83,8 +84,10 @@ def get_neighbors(
bounds = [list(i) for i in geom_shifted.bounds]
_, tan_dirs = Geometry.pop_axis([0, 1, 2], axis=axis)
for dim in tan_dirs:
bounds[0][dim] = increment_float(bounds[0][dim], 1.0)
bounds[1][dim] = increment_float(bounds[1][dim], -1.0)
# Don't shrink if the width is already close to 0
if not isclose(bounds[0][dim], bounds[1][dim], rel_tol=2 * fp_eps):
bounds[0][dim] = increment_float(bounds[0][dim], 1.0)
bounds[1][dim] = increment_float(bounds[1][dim], -1.0)

structures_side = Scene.intersecting_structures(Box.from_bounds(*bounds), structures)

Expand Down
Loading

0 comments on commit dd4cca8

Please sign in to comment.