bf.geometry¶
geometry ¶
Shared geometry and dispersion helpers for :mod:bifrost.stack.
- :func:
pack_polygonsand :func:signed_distance: a pure-JAX (jit-safe) signed distance to the union of a layer's polygons, differentiable with respect to the vertices. Each polygon is filled by the nonzero winding rule, overlapping polygons merge without seams, and KLayout hole cut lines and seams of abutting polygons are not treated as boundaries. :func:split_t_junctionsmakes partly shared junction edges (a narrower taper on a waveguide, a taper on the side of an MMI) exactly shared, so they are seams too. :func:topology_changestells whether moved vertices still fit that static topology. :func:nearest_edgesfinds the few boundary edges nearest to each point (for the exact fill at polygon corners). - :func:
derive_polygonsand :func:run_recipe: the exact polygons of a boolean of layers (and / or / not / xor), built once as a recipe whose vertices are copies of layer vertices or crossings of layer edges, and evaluated from traced vertices (jit-safe, differentiable). - :func:
gaussian_random_field: Gaussian random fields for surface roughness. - :func:
align_material_poles/ :class:SlotPole: give all materials the same dispersive pole slots, so that mixing materials in a voxel mixes only the pole strengths (exact susceptibility mixing in fdtdx >= 0.6.2). - :func:
warn_reversible_losses: warns when fdtdx's reversible gradients meet lossy materials.
SlotPole ¶
Bases: Pole
A pole given directly by its ADE constants (omega_0, gamma, K).
Used by :func:align_material_poles: every material of a device gets the
same ordered slots, and a material that lacks a pole has strength = 0
in that slot.
Source code in bifrost/geometry.py
signed_distance ¶
signed_distance(points: Array, a: Array, b: Array, poly_ids: ndarray, boundary: ndarray, chunk: int = 64, sign_tol: float = 0.001) -> Array
Signed distance (negative inside) from points to the union of polygons.
Pure JAX, safe under jit. Each polygon is filled by the nonzero winding
rule (orientation does not matter) and the union distance is the minimum of
the per-polygon signed distances, so overlapping polygons do not leave seams.
The edges are streamed in chunks and every polygon is folded into the union as soon as its last edge has been seen, so memory is ~ (chunk + 6) * P no matter how many polygons there are; compute is ~ E * P.
Where the nearest boundary point lies inside an edge (or the point lies on a vertex), the value is the signed distance to that edge's line (oriented with the polygon; averaged over tied edges), so the derivative stays correct on the boundary itself; near vertices it is sign * |p - v|.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
Array
|
(P, 2) query points. |
required |
a
|
Array
|
(E, 2) edge start points. |
required |
b
|
Array
|
(E, 2) edge end points. |
required |
poly_ids
|
ndarray
|
(E,) static polygon index of each edge (non-decreasing). |
required |
boundary
|
ndarray
|
(E,) static mask of the edges that count for the distance. |
required |
chunk
|
int
|
edges processed per step. |
64
|
sign_tol
|
float
|
distance (points' units) from an edge's line within which the line's side is trusted over the winding number (round-off on the boundary); beyond it the winding number signs the distance. |
0.001
|
Source code in bifrost/geometry.py
1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 | |
pack_polygons ¶
Concatenates (N_i, 2) vertex arrays into one (sum N_i, 2) array.
Returns:
| Name | Type | Description |
|---|---|---|
vertices |
Array
|
the packed (differentiable) vertices. |
sizes |
tuple[int, ...]
|
number of vertices per polygon (the static topology). |
Source code in bifrost/geometry.py
split_t_junctions ¶
split_t_junctions(vertices: ndarray, sizes: Sequence[int], tol: float = 1e-06) -> tuple[np.ndarray, tuple[int, ...]]
Inserts into every edge the polygon vertices that lie on it (T-junctions).
Where a polygon corner lies inside another edge, e.g. a taper end in the
middle of the side of an MMI, the two polygons share only part of that edge,
so the shared part is not recognized as a seam. Splitting the long edge at
the corner makes the shared part an exactly coincident (reversed) edge, which
:func:_edge_topology then excludes from the distance. Inserted points lie
on their edges, so the filled region does not change.
Static (numpy, from concrete vertices); the inserted points copy vertices, so
with vertices[index] they follow those vertices when they move, which is
right as long as they stay inside their edges (see :func:topology_changes).
A vertex up to tol off an edge is inserted too: the edge then bends
through it by at most tol, where leaving it out would leave a slit
between the polygons (layout snapping moves a vertex off a slanted edge it
was on).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
ndarray
|
(E, 2) packed polygon vertices. |
required |
sizes
|
Sequence[int]
|
vertices per polygon. |
required |
tol
|
float
|
distance (same units as |
1e-06
|
Returns:
| Name | Type | Description |
|---|---|---|
index |
ndarray
|
indices into |
sizes |
tuple[int, ...]
|
vertices per split polygon. |
Source code in bifrost/geometry.py
gaussian_random_field ¶
gaussian_random_field(shape: tuple[int, int], correlation_length: float | tuple[float, float], key: Array) -> Array
Unit-variance Gaussian random field with covariance exp(-r^2 / L^2).
White noise filtered in Fourier space on a grid padded by 3 L on each side,
so the field is not periodic over shape. Lengths are in cells; a pair
(Lx, Ly) is one length on cells of different sizes along x and y
(covariance exp(-(x / Lx)^2 - (y / Ly)^2) in cells).
Source code in bifrost/geometry.py
align_material_poles ¶
Gives every material the same ordered pole slots.
The slots are the distinct (omega_0, gamma) pairs of all poles of all
materials, in order of first appearance; poles of one material that share
(omega_0, gamma) are merged (their K add, which is exact). A material gets
its own K in its slots and 0 elsewhere, so the discrete coefficients c1, c2
of a slot are the same for every material and only c3 (proportional to K)
differs. Mixing materials in a voxel then reduces to mixing c3, which mixes
the susceptibilities exactly: chi = sum_m w_m chi_m.
Materials without poles stay unchanged when no material is dispersive. Permittivity (eps_inf), permeability and conductivities are kept, so fdtdx's material order does not change. Idempotent.
Source code in bifrost/geometry.py
warn_reversible_losses ¶
warn_reversible_losses(materials: dict[str, Material], config: SimulationConfig, name: str, conductivity: bool) -> None
Warns about lossy materials when gradients use fdtdx's reversible mode.
The reversible backward pass runs the update backwards in time, which
amplifies round-off in lossy voxels: by ~exp(gamma T / 2) for a damped
Lorentz pole, ~exp(gamma T) for a Drude pole, and per step for a
conductivity. Use GradientConfig(method="checkpointed") for those.