fdtdx 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. fdtdx/__init__.py +37 -0
  2. fdtdx/constraints/__init__.py +14 -0
  3. fdtdx/constraints/binary_transform.py +407 -0
  4. fdtdx/constraints/discrete.py +407 -0
  5. fdtdx/constraints/mapping.py +102 -0
  6. fdtdx/constraints/module.py +268 -0
  7. fdtdx/constraints/pillars.py +140 -0
  8. fdtdx/constraints/utils.py +186 -0
  9. fdtdx/core/__init__.py +15 -0
  10. fdtdx/core/config.py +153 -0
  11. fdtdx/core/conversion/__init__.py +14 -0
  12. fdtdx/core/conversion/export.py +169 -0
  13. fdtdx/core/conversion/import_utils.py +57 -0
  14. fdtdx/core/conversion/load_stl.py +55 -0
  15. fdtdx/core/gaussian_blur.py +148 -0
  16. fdtdx/core/jax/__init__.py +14 -0
  17. fdtdx/core/jax/pytrees.py +307 -0
  18. fdtdx/core/jax/sharding.py +134 -0
  19. fdtdx/core/jax/ste.py +33 -0
  20. fdtdx/core/jax/typing.py +58 -0
  21. fdtdx/core/jax/utils.py +36 -0
  22. fdtdx/core/misc.py +753 -0
  23. fdtdx/core/modes.py +135 -0
  24. fdtdx/core/physics/__init__.py +14 -0
  25. fdtdx/core/physics/constants.py +119 -0
  26. fdtdx/core/physics/losses.py +45 -0
  27. fdtdx/core/physics/metrics.py +86 -0
  28. fdtdx/core/physics/modes.py +119 -0
  29. fdtdx/core/plotting/__init__.py +14 -0
  30. fdtdx/core/plotting/colors.py +40 -0
  31. fdtdx/core/plotting/debug.py +98 -0
  32. fdtdx/core/plotting/device_permittivity_index_utils.py +148 -0
  33. fdtdx/core/plotting/utils.py +89 -0
  34. fdtdx/fdtd/__init__.py +17 -0
  35. fdtdx/fdtd/backward.py +139 -0
  36. fdtdx/fdtd/curl.py +120 -0
  37. fdtdx/fdtd/fdtd.py +420 -0
  38. fdtdx/fdtd/forward.py +196 -0
  39. fdtdx/fdtd/update.py +453 -0
  40. fdtdx/interfaces/__init__.py +0 -0
  41. fdtdx/interfaces/modules.py +227 -0
  42. fdtdx/interfaces/recorder.py +321 -0
  43. fdtdx/interfaces/state.py +82 -0
  44. fdtdx/interfaces/time_filter.py +576 -0
  45. fdtdx/objects/__init__.py +14 -0
  46. fdtdx/objects/boundaries/__init__.py +14 -0
  47. fdtdx/objects/boundaries/boundary_utils.py +306 -0
  48. fdtdx/objects/boundaries/initialization.py +219 -0
  49. fdtdx/objects/boundaries/perfectly_matched_layer.py +415 -0
  50. fdtdx/objects/container.py +175 -0
  51. fdtdx/objects/detectors/__init__.py +14 -0
  52. fdtdx/objects/detectors/detector.py +402 -0
  53. fdtdx/objects/detectors/energy.py +106 -0
  54. fdtdx/objects/detectors/phasor.py +142 -0
  55. fdtdx/objects/detectors/plotting/__init__.py +14 -0
  56. fdtdx/objects/detectors/plotting/line_plot.py +28 -0
  57. fdtdx/objects/detectors/plotting/plot2d.py +148 -0
  58. fdtdx/objects/detectors/plotting/video.py +166 -0
  59. fdtdx/objects/detectors/poynting_flux.py +103 -0
  60. fdtdx/objects/initialization.py +630 -0
  61. fdtdx/objects/material.py +130 -0
  62. fdtdx/objects/multi_material/__init__.py +14 -0
  63. fdtdx/objects/multi_material/device.py +174 -0
  64. fdtdx/objects/multi_material/fiber.py +164 -0
  65. fdtdx/objects/multi_material/multi_material.py +169 -0
  66. fdtdx/objects/multi_material/random_scatterer.py +202 -0
  67. fdtdx/objects/object.py +811 -0
  68. fdtdx/objects/sources/__init__.py +14 -0
  69. fdtdx/objects/sources/plane_source.py +926 -0
  70. fdtdx/objects/sources/source.py +258 -0
  71. fdtdx/objects/wavelength.py +112 -0
  72. fdtdx/py.typed +0 -0
  73. fdtdx/shared/__init__.py +0 -0
  74. fdtdx/shared/logger.py +328 -0
  75. fdtdx/shared/misc.py +70 -0
  76. fdtdx/shared/plot_setup.py +166 -0
  77. fdtdx-0.1.0.dist-info/METADATA +86 -0
  78. fdtdx-0.1.0.dist-info/RECORD +80 -0
  79. fdtdx-0.1.0.dist-info/WHEEL +4 -0
  80. fdtdx-0.1.0.dist-info/licenses/LICENSE +21 -0
fdtdx/__init__.py ADDED
@@ -0,0 +1,37 @@
1
+ """FDTDX: A JAX-based Finite-Difference Time-Domain (FDTD) Electromagnetic Simulation Package.
2
+
3
+ A high-performance framework for electromagnetic simulations and inverse design optimization
4
+ using the FDTD method, implemented in JAX for GPU acceleration and automatic differentiation.
5
+
6
+ Key Features:
7
+ - Memory-efficient automatic differentiation using time-reversibility
8
+ - Multi-GPU support for large-scale simulations
9
+ - Intuitive object positioning with relative coordinates
10
+ - Built-in optimization capabilities for inverse design
11
+ - Comprehensive boundary condition implementations (e.g., PML)
12
+ - Field detectors and visualization tools
13
+ - JIT-compiled operations for maximum performance
14
+
15
+ Example:
16
+ Basic simulation setup:
17
+ ```python
18
+ import fdtdx
19
+ from fdtdx.objects import Source, Detector
20
+
21
+ # Create simulation components
22
+ source = Source(...)
23
+ detector = Detector(...)
24
+
25
+ # Run simulation
26
+ fields = fdtdx.simulate(source, detector)
27
+ ```
28
+
29
+ Notes:
30
+ The package is designed for both research and industrial applications in nanophotonics,
31
+ metamaterials, and photonic integrated circuits. It supports both 2D and 3D simulations
32
+ with automatic gradient computation for optimization tasks.
33
+
34
+ References:
35
+ - Paper: https://github.com/ymahlau/fdtdx
36
+ - Documentation: https://github.com/ymahlau/fdtdx/docs
37
+ """
@@ -0,0 +1,14 @@
1
+ """Constraint modules for enforcing physical and fabrication requirements.
2
+
3
+ This package provides constraint modules that can be applied during optimization
4
+ to ensure designs meet physical realizability and fabrication requirements.
5
+ Key constraints include:
6
+
7
+ - Minimum feature size constraints
8
+ - Binary material constraints (air/dielectric only)
9
+ - Connectivity constraints (no floating material)
10
+ - Fabrication constraints (no trapped air, etc.)
11
+
12
+ The constraints are implemented as modules that can be chained together
13
+ and applied during the optimization process.
14
+ """
@@ -0,0 +1,407 @@
1
+ import jax
2
+ import jax.numpy as jnp
3
+
4
+ from fdtdx.core.misc import PaddingConfig, advanced_padding
5
+
6
+
7
+ def remove_floating_polymer(
8
+ matrix: jax.Array, # 1 is polymer, zero is air, shape (x, y, z)
9
+ ) -> jax.Array:
10
+ """Removes polymer regions that are not connected to the substrate.
11
+
12
+ Uses flood-fill algorithm to identify polymer regions connected to the bottom layer
13
+ and removes any floating polymer regions that are not connected.
14
+
15
+ Args:
16
+ matrix: Binary array where 1 represents polymer and 0 represents air.
17
+ Shape is (x, y, z) representing the 3D grid.
18
+
19
+ Returns:
20
+ Modified binary array with floating polymer regions removed.
21
+ """
22
+ connected = compute_polymer_connection(matrix)
23
+ non_connected_polymer = jnp.invert(connected) & matrix
24
+ matrix = matrix & jnp.invert(non_connected_polymer)
25
+ return matrix
26
+
27
+
28
+ def remove_polymer_non_connected_to_x_max_middle(
29
+ matrix: jax.Array, # 1 is polymer, zero is air, shape (x, y, z)
30
+ ) -> jax.Array:
31
+ """Removes polymer regions not connected to the middle of the x-max boundary.
32
+
33
+ Uses flood-fill algorithm starting from the middle point of the x-max boundary
34
+ to identify connected polymer regions and removes unconnected regions.
35
+
36
+ Args:
37
+ matrix: Binary array where 1 represents polymer and 0 represents air.
38
+ Shape is (x, y, z) representing the 3D grid.
39
+
40
+ Returns:
41
+ Modified binary array with unconnected polymer regions removed.
42
+ """
43
+ y_middle = round(matrix.shape[1] / 2)
44
+ x_middle = round(matrix.shape[0] / 2)
45
+ connected = compute_polymer_connection(
46
+ matrix,
47
+ connected_slice=(x_middle, y_middle, None),
48
+ )
49
+ non_connected_polymer = jnp.invert(connected) & matrix
50
+ matrix = matrix & jnp.invert(non_connected_polymer)
51
+ return matrix
52
+
53
+
54
+ def connect_holes_and_structures(
55
+ matrix: jax.Array, # 1 is polymer, zero is air, shape (x, y, z)
56
+ ) -> jax.Array:
57
+ """Connects disconnected polymer structures and air holes in the matrix.
58
+
59
+ Performs a two-pass algorithm:
60
+ 1. Connects disconnected polymer structures by adding polymer material
61
+ 2. Connects disconnected air regions by removing polymer material
62
+
63
+ This ensures both structural integrity and proper air ventilation.
64
+
65
+ Args:
66
+ matrix: Binary array where 1 represents polymer and 0 represents air.
67
+ Shape is (x, y, z) representing the 3D grid.
68
+
69
+ Returns:
70
+ Modified binary array with connected structures and holes.
71
+ """
72
+ matrix = matrix.astype(bool)
73
+ # first connect all polymer structures # TODO: convert to fori-loops
74
+ for i in range(matrix.shape[-1] - 1):
75
+ connected = compute_polymer_connection(matrix)
76
+ if i == 0:
77
+ lower_slice = jnp.ones_like(matrix[..., 0])
78
+ else:
79
+ lower_slice = matrix[..., i - 1]
80
+ new_middle, new_upper = connect_slice(
81
+ lower_slice=lower_slice,
82
+ middle_slice=matrix[..., i],
83
+ upper_slice=matrix[..., i + 1],
84
+ upper_save_points=connected[..., i + 1],
85
+ )
86
+ matrix = matrix.at[..., i].set(new_middle)
87
+ matrix = matrix.at[..., i + 1].set(new_upper)
88
+ # then connect all air
89
+ for i in range(matrix.shape[-1], 0, -1):
90
+ air_connected = compute_air_connection(matrix)
91
+ if i == matrix.shape[-1]:
92
+ lower_slice = jnp.ones_like(matrix[..., 0])
93
+ else:
94
+ lower_slice = jnp.invert(matrix[..., i + 1])
95
+ new_middle, new_upper = connect_slice(
96
+ lower_slice=lower_slice,
97
+ middle_slice=jnp.invert(matrix[..., i]),
98
+ upper_slice=jnp.invert(matrix[..., i - 1]),
99
+ upper_save_points=air_connected[..., i - 1],
100
+ )
101
+ matrix = matrix.at[..., i].set(jnp.invert(new_middle))
102
+ matrix = matrix.at[..., i - 1].set(jnp.invert(new_upper))
103
+ # throw away all polymer that got free floating due to air connection in last step
104
+ connected = compute_polymer_connection(matrix)
105
+ non_connected_polymer = jnp.invert(connected) & matrix
106
+ matrix = matrix & jnp.invert(non_connected_polymer)
107
+ return matrix
108
+
109
+
110
+ def compute_air_connection(matrix: jax.Array) -> jax.Array:
111
+ """Computes a mask of air regions connected to the boundaries.
112
+
113
+ Uses iterative dilation to identify air regions (zeros in the matrix)
114
+ that are connected to the simulation boundaries. This is used to ensure
115
+ proper ventilation in the structure.
116
+
117
+ Args:
118
+ matrix: Binary array where 1 represents polymer and 0 represents air.
119
+
120
+ Returns:
121
+ Boolean array marking air regions connected to boundaries.
122
+ """
123
+ inv_matrix = jnp.invert(matrix)
124
+ n = max([matrix.shape[0], matrix.shape[1], matrix.shape[2]])
125
+ n4_kernel = jnp.asarray(
126
+ [
127
+ [0, 1, 0],
128
+ [1, 1, 1],
129
+ [0, 1, 0],
130
+ ],
131
+ dtype=bool,
132
+ )
133
+ connected = jnp.zeros_like(inv_matrix, dtype=bool)
134
+ connected = connected.at[:, :, -1].set(True)
135
+ connected = connected.at[0, :, :].set(True)
136
+ connected = connected.at[-1, :, :].set(True)
137
+ connected = connected.at[:, 0, :].set(True)
138
+ connected = connected.at[:, -1, :].set(True)
139
+ connected = connected & inv_matrix
140
+
141
+ def _body_fn(_, arr):
142
+ arr = seperated_3d_dilation(
143
+ arr_3d=arr,
144
+ kernel_xy=n4_kernel,
145
+ kernel_xz=n4_kernel,
146
+ kernel_yz=n4_kernel,
147
+ reduction_arr=inv_matrix,
148
+ )
149
+ return arr
150
+
151
+ connected = jax.lax.fori_loop(0, n, _body_fn, connected)
152
+
153
+ return connected
154
+
155
+
156
+ def compute_polymer_connection(
157
+ matrix: jax.Array,
158
+ connected_slice: tuple[int | None, int | None, int | None] | None = None,
159
+ ) -> jax.Array:
160
+ """Computes a mask of polymer regions connected to specified points.
161
+
162
+ Uses iterative dilation to identify polymer regions (ones in the matrix)
163
+ that are connected either to the bottom layer or to specified points
164
+ in connected_slice.
165
+
166
+ Args:
167
+ matrix: Binary array where 1 represents polymer and 0 represents air.
168
+ connected_slice: Optional tuple of indices specifying starting points
169
+ for the connection computation. If None, uses bottom layer.
170
+
171
+ Returns:
172
+ Boolean array marking connected polymer regions.
173
+ """
174
+ n = max([matrix.shape[0], matrix.shape[1], matrix.shape[2]])
175
+ padded = False
176
+ if matrix.shape[2] == 1:
177
+ padded = True
178
+ matrix = jnp.pad(matrix, pad_width=((0, 0), (0, 0), (1, 1)))
179
+ n4_kernel = jnp.asarray(
180
+ [
181
+ [0, 1, 0],
182
+ [1, 1, 1],
183
+ [0, 1, 0],
184
+ ],
185
+ dtype=bool,
186
+ )
187
+ connected = jnp.zeros_like(matrix, dtype=bool)
188
+ if connected_slice is None:
189
+ connected = connected.at[..., 0].set(True)
190
+ else:
191
+ connected = connected.at[connected_slice].set(True)
192
+
193
+ def _body_fn(_, arr):
194
+ arr = seperated_3d_dilation(
195
+ arr_3d=arr,
196
+ kernel_xy=n4_kernel,
197
+ kernel_xz=n4_kernel,
198
+ kernel_yz=n4_kernel,
199
+ reduction_arr=matrix,
200
+ )
201
+ return arr
202
+
203
+ connected = jax.lax.fori_loop(0, n, _body_fn, connected)
204
+
205
+ if padded:
206
+ connected = connected[..., 1:2]
207
+ return connected
208
+
209
+
210
+ def connect_slice(
211
+ lower_slice: jax.Array,
212
+ middle_slice: jax.Array, # this layer is optimized
213
+ upper_slice: jax.Array, # this layer is optimized,
214
+ upper_save_points: jax.Array, # points which are surely connected
215
+ ) -> tuple[jax.Array, jax.Array]:
216
+ """Connects polymer regions between adjacent slices.
217
+
218
+ Attempts to connect disconnected polymer regions between three adjacent
219
+ z-slices by modifying the middle and upper slices while preserving
220
+ specified connection points.
221
+
222
+ Args:
223
+ lower_slice: Binary array representing the lower z-slice.
224
+ middle_slice: Binary array representing the middle z-slice to be optimized.
225
+ upper_slice: Binary array representing the upper z-slice to be optimized.
226
+ upper_save_points: Boolean mask of points in upper slice that must remain connected.
227
+
228
+ Returns:
229
+ Tuple of (modified_middle_slice, modified_upper_slice) with connected regions.
230
+ """
231
+ n = max(lower_slice.shape[0], lower_slice.shape[1])
232
+
233
+ # define kernels
234
+ n4_kernel = jnp.asarray(
235
+ [
236
+ [0, 1, 0],
237
+ [1, 1, 1],
238
+ [0, 1, 0],
239
+ ],
240
+ dtype=bool,
241
+ )
242
+ n8_kernel = jnp.asarray(
243
+ [
244
+ [1, 1, 1],
245
+ [1, 1, 1],
246
+ [1, 1, 1],
247
+ ],
248
+ dtype=bool,
249
+ )
250
+ direction_kernels = jnp.asarray(
251
+ [
252
+ [[0, 1, 0], [0, 0, 0], [0, 0, 0]],
253
+ [[0, 0, 0], [0, 0, 1], [0, 0, 0]],
254
+ [[0, 0, 0], [0, 0, 0], [0, 1, 0]],
255
+ [[0, 0, 0], [1, 0, 0], [0, 0, 0]],
256
+ ],
257
+ dtype=bool,
258
+ )
259
+
260
+ # first determine all well-supported points
261
+ connected_points = upper_slice & middle_slice
262
+ connected_points = connected_points | upper_save_points
263
+
264
+ for _ in range(n):
265
+ connected_points = dilate_jax(connected_points, n4_kernel)
266
+ connected_points = connected_points & upper_slice
267
+ upper_air = jnp.invert(upper_slice)
268
+ non_connected_points = jnp.invert(upper_air | connected_points)
269
+
270
+ # first try to connect by setting additional polymers in lower array
271
+ connectable_region_lower = dilate_jax(middle_slice, n4_kernel)
272
+ connectable_region_lower = connectable_region_lower | lower_slice # connectable either vertical or horiz.
273
+ connectable_by_lower = non_connected_points & connectable_region_lower
274
+ # update matrix, non-connected and connected points
275
+ middle_slice = middle_slice | connectable_by_lower
276
+ connected_points = connected_points | connectable_by_lower
277
+ for _ in range(n):
278
+ connected_points = dilate_jax(connected_points, n4_kernel)
279
+ connected_points = connected_points & upper_slice
280
+ non_connected_points = jnp.invert(upper_air | connected_points)
281
+
282
+ # then try to connect by adding polymer in upper array
283
+ connectable_region_upper = dilate_jax(connected_points, n8_kernel)
284
+ connectable_by_upper = non_connected_points & connectable_region_upper
285
+ connection_points = jax.vmap(dilate_jax, in_axes=(None, 0))(connectable_by_upper, direction_kernels)
286
+ valid_connection_points = connectable_region_upper & connection_points
287
+ valid_connection_points = jnp.sum(valid_connection_points, axis=0).astype(bool)
288
+ # update matrix, non-connected and connected points
289
+ upper_slice = upper_slice | valid_connection_points
290
+ for _ in range(n):
291
+ connected_points = dilate_jax(connected_points, n4_kernel)
292
+ connected_points = connected_points & upper_slice
293
+ non_connected_points = jnp.invert(upper_air | connected_points)
294
+
295
+ # delete all non-connected
296
+ upper_slice = upper_slice & jnp.invert(non_connected_points)
297
+
298
+ return middle_slice, upper_slice
299
+
300
+
301
+ def dilate_jax(image: jax.Array, kernel: jax.Array) -> jax.Array:
302
+ """Performs morphological dilation on a binary image using JAX.
303
+
304
+ Args:
305
+ image: Binary input array to be dilated.
306
+ kernel: Binary kernel defining the dilation shape.
307
+
308
+ Returns:
309
+ Dilated binary array.
310
+ """
311
+ conv = jax.scipy.signal.convolve2d(image, kernel, mode="same", boundary="fill")
312
+ binary_arr = jnp.asarray(conv, dtype=bool)
313
+ return binary_arr
314
+
315
+
316
+ def erode_jax(image: jax.Array, kernel: jax.Array) -> jax.Array:
317
+ """Performs morphological erosion on a binary image using JAX.
318
+
319
+ Args:
320
+ image: Binary input array to be eroded.
321
+ kernel: Binary kernel defining the erosion shape.
322
+
323
+ Returns:
324
+ Eroded binary array.
325
+ """
326
+ conv = jax.scipy.signal.convolve2d(~image, kernel, mode="same", boundary="fill")
327
+ binary_arr = jnp.asarray(conv, dtype=bool)
328
+ return ~binary_arr
329
+
330
+
331
+ def seperated_3d_dilation(
332
+ arr_3d: jax.Array,
333
+ kernel_xy: jax.Array,
334
+ kernel_yz: jax.Array,
335
+ kernel_xz: jax.Array,
336
+ reduction_arr: jax.Array,
337
+ ) -> jax.Array:
338
+ """Performs separated 3D dilation along each axis plane.
339
+
340
+ Applies 2D dilation kernels separately in the XY, YZ, and XZ planes
341
+ to approximate 3D dilation while being more computationally efficient.
342
+ The result is masked by the reduction array after each operation.
343
+
344
+ Args:
345
+ arr_3d: 3D binary array to be dilated.
346
+ kernel_xy: 2D kernel for XY plane dilation.
347
+ kernel_yz: 2D kernel for YZ plane dilation.
348
+ kernel_xz: 2D kernel for XZ plane dilation.
349
+ reduction_arr: Binary mask to constrain dilation results.
350
+
351
+ Returns:
352
+ Dilated 3D binary array.
353
+ """
354
+
355
+ def convolve_partial(image: jax.Array, kernel: jax.Array):
356
+ return jax.scipy.signal.convolve2d(image, kernel, mode="same", boundary="fill")
357
+
358
+ arr_3d = jax.vmap(convolve_partial, in_axes=(2, None), out_axes=(2))(arr_3d, kernel_xy)
359
+ arr_3d = jnp.asarray(arr_3d, dtype=bool)
360
+ arr_3d = arr_3d & reduction_arr
361
+
362
+ arr_3d = jax.vmap(convolve_partial, in_axes=(1, None), out_axes=(1))(arr_3d, kernel_xz)
363
+ arr_3d = jnp.asarray(arr_3d, dtype=bool)
364
+ arr_3d = arr_3d & reduction_arr
365
+
366
+ arr_3d = jax.vmap(convolve_partial, in_axes=(0, None), out_axes=(0))(arr_3d, kernel_yz)
367
+ arr_3d = jnp.asarray(arr_3d, dtype=bool)
368
+ arr_3d = arr_3d & reduction_arr
369
+
370
+ return arr_3d
371
+
372
+
373
+ def binary_median_filter(
374
+ arr_3d: jax.Array,
375
+ kernel_sizes: tuple[int, int, int],
376
+ padding_cfg: PaddingConfig,
377
+ ) -> jax.Array:
378
+ """Applies a binary median filter to a 3D array.
379
+
380
+ Implements a median filter for binary data by using convolution
381
+ and thresholding. The filter is applied separately along each axis
382
+ using the specified kernel sizes.
383
+
384
+ Args:
385
+ arr_3d: 3D binary input array.
386
+ kernel_sizes: Tuple of (kx, ky, kz) specifying filter size in each dimension.
387
+ padding_cfg: Configuration for padding the input array.
388
+
389
+ Returns:
390
+ Filtered binary array.
391
+ """
392
+ # padding
393
+ padded_arr, orig_slice = advanced_padding(arr_3d, padding_cfg)
394
+ padded_arr = padded_arr.astype(jnp.float32)
395
+ # create filter kernel and convolve (avg filter)
396
+ for axis, k_size in enumerate(kernel_sizes):
397
+ shape_list = [1, 1, 1]
398
+ shape_list[axis] = k_size
399
+ shape_tpl = tuple(shape_list)
400
+ kernel = jnp.ones(shape=shape_tpl, dtype=jnp.float32)
401
+ padded_arr = jax.scipy.signal.convolve(padded_arr, kernel, mode="same", method="direct")
402
+ # discretize again to get median
403
+ kernel_sum = jnp.prod(jnp.asarray(kernel_sizes))
404
+ result = padded_arr / kernel_sum
405
+ result = result[orig_slice]
406
+ result = jnp.round(result).astype(arr_3d.dtype)
407
+ return result