prefab 1.3.0__py3-none-any.whl → 1.4.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.
- prefab/__init__.py +1 -1
- prefab/__main__.py +29 -24
- prefab/compare.py +49 -61
- prefab/device.py +163 -394
- prefab/geometry.py +102 -137
- prefab/models.py +19 -48
- prefab/predict.py +68 -55
- prefab/py.typed +0 -0
- prefab/read.py +57 -303
- prefab/shapes.py +357 -187
- {prefab-1.3.0.dist-info → prefab-1.4.0.dist-info}/METADATA +21 -35
- prefab-1.4.0.dist-info/RECORD +15 -0
- prefab-1.3.0.dist-info/RECORD +0 -14
- {prefab-1.3.0.dist-info → prefab-1.4.0.dist-info}/WHEEL +0 -0
- {prefab-1.3.0.dist-info → prefab-1.4.0.dist-info}/entry_points.txt +0 -0
- {prefab-1.3.0.dist-info → prefab-1.4.0.dist-info}/licenses/LICENSE +0 -0
prefab/read.py
CHANGED
|
@@ -1,22 +1,52 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
Functions to create Device objects from various data sources.
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
This module provides utilities for loading device geometries from multiple formats,
|
|
5
|
+
including image files, numpy arrays, and GDS layout files. All functions return
|
|
6
|
+
Device objects with optional preprocessing capabilities.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any, cast
|
|
5
10
|
|
|
6
11
|
import cv2
|
|
7
12
|
import gdstk
|
|
8
13
|
import numpy as np
|
|
9
14
|
|
|
10
15
|
from . import geometry
|
|
11
|
-
from .device import
|
|
16
|
+
from .device import Device
|
|
17
|
+
|
|
18
|
+
# Conversion factor from GDS units (micrometers) to nanometers
|
|
19
|
+
_GDS_UM_TO_NM = 1000
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _binarize_if_needed(
|
|
23
|
+
device_array: np.ndarray[Any, Any], binarize: bool
|
|
24
|
+
) -> np.ndarray[Any, Any]:
|
|
25
|
+
"""
|
|
26
|
+
Conditionally binarize a device array.
|
|
12
27
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
device_array : np.ndarray
|
|
31
|
+
The array to potentially binarize.
|
|
32
|
+
binarize : bool
|
|
33
|
+
If True, binarize the array using hard thresholding.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
np.ndarray
|
|
38
|
+
The binarized array if binarize is True, otherwise the original array.
|
|
39
|
+
"""
|
|
40
|
+
if binarize:
|
|
41
|
+
return geometry.binarize_hard(np.asarray(device_array, dtype=np.float64))
|
|
42
|
+
return device_array
|
|
16
43
|
|
|
17
44
|
|
|
18
45
|
def from_ndarray(
|
|
19
|
-
ndarray: np.ndarray,
|
|
46
|
+
ndarray: np.ndarray[Any, Any],
|
|
47
|
+
resolution: float = 1.0,
|
|
48
|
+
binarize: bool = True,
|
|
49
|
+
**kwargs: Any,
|
|
20
50
|
) -> Device:
|
|
21
51
|
"""
|
|
22
52
|
Create a Device from an ndarray.
|
|
@@ -46,13 +76,12 @@ def from_ndarray(
|
|
|
46
76
|
device_array = cv2.resize(
|
|
47
77
|
device_array, dsize=(0, 0), fx=resolution, fy=resolution
|
|
48
78
|
)
|
|
49
|
-
|
|
50
|
-
device_array = geometry.binarize_hard(device_array)
|
|
79
|
+
device_array = _binarize_if_needed(device_array, binarize)
|
|
51
80
|
return Device(device_array=device_array, **kwargs)
|
|
52
81
|
|
|
53
82
|
|
|
54
83
|
def from_img(
|
|
55
|
-
img_path: str, img_width_nm:
|
|
84
|
+
img_path: str, img_width_nm: int | None = None, binarize: bool = True, **kwargs: Any
|
|
56
85
|
) -> Device:
|
|
57
86
|
"""
|
|
58
87
|
Create a Device from an image file.
|
|
@@ -83,8 +112,7 @@ def from_img(
|
|
|
83
112
|
device_array = cv2.resize(
|
|
84
113
|
device_array, dsize=(0, 0), fx=resolution, fy=resolution
|
|
85
114
|
)
|
|
86
|
-
|
|
87
|
-
device_array = geometry.binarize_hard(device_array)
|
|
115
|
+
device_array = _binarize_if_needed(device_array, binarize)
|
|
88
116
|
return Device(device_array=device_array, **kwargs)
|
|
89
117
|
|
|
90
118
|
|
|
@@ -92,8 +120,8 @@ def from_gds(
|
|
|
92
120
|
gds_path: str,
|
|
93
121
|
cell_name: str,
|
|
94
122
|
gds_layer: tuple[int, int] = (1, 0),
|
|
95
|
-
bounds:
|
|
96
|
-
**kwargs,
|
|
123
|
+
bounds: tuple[tuple[float, float], tuple[float, float]] | None = None,
|
|
124
|
+
**kwargs: Any,
|
|
97
125
|
) -> Device:
|
|
98
126
|
"""
|
|
99
127
|
Create a Device from a GDS cell.
|
|
@@ -121,7 +149,7 @@ def from_gds(
|
|
|
121
149
|
processing based on the specified layer.
|
|
122
150
|
"""
|
|
123
151
|
gdstk_library = gdstk.read_gds(gds_path)
|
|
124
|
-
gdstk_cell = gdstk_library[cell_name] #
|
|
152
|
+
gdstk_cell = cast(gdstk.Cell, gdstk_library[cell_name]) # pyright: ignore[reportIndexIssue]
|
|
125
153
|
device_array = _gdstk_to_device_array(
|
|
126
154
|
gdstk_cell=gdstk_cell, gds_layer=gds_layer, bounds=bounds
|
|
127
155
|
)
|
|
@@ -131,9 +159,9 @@ def from_gds(
|
|
|
131
159
|
def from_gdstk(
|
|
132
160
|
gdstk_cell: gdstk.Cell,
|
|
133
161
|
gds_layer: tuple[int, int] = (1, 0),
|
|
134
|
-
bounds:
|
|
135
|
-
**kwargs,
|
|
136
|
-
):
|
|
162
|
+
bounds: tuple[tuple[float, float], tuple[float, float]] | None = None,
|
|
163
|
+
**kwargs: Any,
|
|
164
|
+
) -> Device:
|
|
137
165
|
"""
|
|
138
166
|
Create a Device from a gdstk cell.
|
|
139
167
|
|
|
@@ -144,7 +172,7 @@ def from_gdstk(
|
|
|
144
172
|
gds_layer : tuple[int, int]
|
|
145
173
|
A tuple specifying the layer and datatype to be used from the cell. Defaults to
|
|
146
174
|
(1, 0).
|
|
147
|
-
bounds : tuple[tuple[float, float], tuple[float, float]]
|
|
175
|
+
bounds : Optional[tuple[tuple[float, float], tuple[float, float]]]
|
|
148
176
|
A tuple specifying the bounds for cropping the cell before conversion, formatted
|
|
149
177
|
as ((min_x, min_y), (max_x, max_y)), in units of the GDS cell. If None, the
|
|
150
178
|
entire cell is used.
|
|
@@ -166,8 +194,8 @@ def from_gdstk(
|
|
|
166
194
|
def _gdstk_to_device_array(
|
|
167
195
|
gdstk_cell: gdstk.Cell,
|
|
168
196
|
gds_layer: tuple[int, int] = (1, 0),
|
|
169
|
-
bounds:
|
|
170
|
-
) -> np.ndarray:
|
|
197
|
+
bounds: tuple[tuple[float, float], tuple[float, float]] | None = None,
|
|
198
|
+
) -> np.ndarray[Any, Any]:
|
|
171
199
|
"""
|
|
172
200
|
Convert a gdstk.Cell to a device array.
|
|
173
201
|
|
|
@@ -195,24 +223,24 @@ def _gdstk_to_device_array(
|
|
|
195
223
|
polygons, position=(bounds[0][1], bounds[1][1]), axis="y"
|
|
196
224
|
)[1]
|
|
197
225
|
bounds = (
|
|
198
|
-
(int(
|
|
199
|
-
(int(
|
|
226
|
+
(int(_GDS_UM_TO_NM * bounds[0][0]), int(_GDS_UM_TO_NM * bounds[0][1])),
|
|
227
|
+
(int(_GDS_UM_TO_NM * bounds[1][0]), int(_GDS_UM_TO_NM * bounds[1][1])),
|
|
200
228
|
)
|
|
201
229
|
else:
|
|
202
230
|
bbox = gdstk_cell.bounding_box()
|
|
203
231
|
if bbox is None:
|
|
204
232
|
raise ValueError("Cell has no geometry, cannot determine bounds.")
|
|
205
233
|
bounds = (
|
|
206
|
-
(float(
|
|
207
|
-
(float(
|
|
234
|
+
(float(_GDS_UM_TO_NM * bbox[0][0]), float(_GDS_UM_TO_NM * bbox[0][1])),
|
|
235
|
+
(float(_GDS_UM_TO_NM * bbox[1][0]), float(_GDS_UM_TO_NM * bbox[1][1])),
|
|
208
236
|
)
|
|
209
237
|
contours = [
|
|
210
238
|
np.array(
|
|
211
239
|
[
|
|
212
240
|
[
|
|
213
241
|
[
|
|
214
|
-
int(
|
|
215
|
-
int(
|
|
242
|
+
int(_GDS_UM_TO_NM * vertex[0] - bounds[0][0]),
|
|
243
|
+
int(_GDS_UM_TO_NM * vertex[1] - bounds[0][1]),
|
|
216
244
|
]
|
|
217
245
|
]
|
|
218
246
|
for vertex in polygon.points
|
|
@@ -224,280 +252,6 @@ def _gdstk_to_device_array(
|
|
|
224
252
|
(int(bounds[1][1] - bounds[0][1]), int(bounds[1][0] - bounds[0][0])),
|
|
225
253
|
dtype=np.uint8,
|
|
226
254
|
)
|
|
227
|
-
cv2.fillPoly(
|
|
255
|
+
_ = cv2.fillPoly(device_array, contours, (1,))
|
|
228
256
|
device_array = np.flipud(device_array)
|
|
229
257
|
return device_array
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def from_gdsfactory(
|
|
233
|
-
component: "gf.Component",
|
|
234
|
-
**kwargs,
|
|
235
|
-
) -> Device:
|
|
236
|
-
"""
|
|
237
|
-
Create a Device from a gdsfactory component.
|
|
238
|
-
|
|
239
|
-
Parameters
|
|
240
|
-
----------
|
|
241
|
-
component : gf.Component
|
|
242
|
-
The gdsfactory component to be converted into a Device object.
|
|
243
|
-
**kwargs
|
|
244
|
-
Additional keyword arguments to be passed to the Device constructor.
|
|
245
|
-
|
|
246
|
-
Returns
|
|
247
|
-
-------
|
|
248
|
-
Device
|
|
249
|
-
A Device object representing the gdsfactory component.
|
|
250
|
-
|
|
251
|
-
Raises
|
|
252
|
-
------
|
|
253
|
-
ImportError
|
|
254
|
-
If the gdsfactory package is not installed.
|
|
255
|
-
"""
|
|
256
|
-
try:
|
|
257
|
-
import gdsfactory as gf # noqa: F401
|
|
258
|
-
except ImportError:
|
|
259
|
-
raise ImportError(
|
|
260
|
-
"The gdsfactory package is required to use this function; "
|
|
261
|
-
"try `pip install gdsfactory`."
|
|
262
|
-
) from None
|
|
263
|
-
|
|
264
|
-
bounds = (
|
|
265
|
-
(component.xmin * 1000, component.ymin * 1000),
|
|
266
|
-
(component.xmax * 1000, component.ymax * 1000),
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
polygons = [
|
|
270
|
-
polygon
|
|
271
|
-
for polygons_list in component.get_polygons_points().values()
|
|
272
|
-
for polygon in polygons_list
|
|
273
|
-
]
|
|
274
|
-
|
|
275
|
-
contours = [
|
|
276
|
-
np.array(
|
|
277
|
-
[
|
|
278
|
-
[[int(1000 * x - bounds[0][0]), int(1000 * y - bounds[0][1])]]
|
|
279
|
-
for x, y in polygon # type: ignore
|
|
280
|
-
]
|
|
281
|
-
)
|
|
282
|
-
for polygon in polygons
|
|
283
|
-
]
|
|
284
|
-
|
|
285
|
-
device_array = np.zeros(
|
|
286
|
-
(int(bounds[1][1] - bounds[0][1]), int(bounds[1][0] - bounds[0][0])),
|
|
287
|
-
dtype=np.uint8,
|
|
288
|
-
)
|
|
289
|
-
cv2.fillPoly(img=device_array, pts=contours, color=(1, 1, 1))
|
|
290
|
-
device_array = np.flipud(device_array)
|
|
291
|
-
return Device(device_array=device_array, **kwargs)
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
def from_sem(
|
|
295
|
-
sem_path: str,
|
|
296
|
-
sem_resolution: Optional[float] = None,
|
|
297
|
-
sem_resolution_key: Optional[str] = None,
|
|
298
|
-
binarize: bool = False,
|
|
299
|
-
bounds: Optional[tuple[tuple[int, int], tuple[int, int]]] = None,
|
|
300
|
-
**kwargs,
|
|
301
|
-
) -> Device:
|
|
302
|
-
"""
|
|
303
|
-
Create a Device from a scanning electron microscope (SEM) image file.
|
|
304
|
-
|
|
305
|
-
Parameters
|
|
306
|
-
----------
|
|
307
|
-
sem_path : str
|
|
308
|
-
The file path to the SEM image.
|
|
309
|
-
sem_resolution : Optional[float]
|
|
310
|
-
The resolution of the SEM image in nanometers per pixel. If not provided, it
|
|
311
|
-
will be extracted from the image metadata using the `sem_resolution_key`.
|
|
312
|
-
sem_resolution_key : Optional[str]
|
|
313
|
-
The key to look for in the SEM image metadata to extract the resolution.
|
|
314
|
-
Required if `sem_resolution` is not provided.
|
|
315
|
-
binarize : bool
|
|
316
|
-
If True, the SEM image will be binarized (converted to binary values) before
|
|
317
|
-
conversion to a Device object. This is needed for processing grayscale images
|
|
318
|
-
into binary masks. Defaults to False.
|
|
319
|
-
bounds : Optional[tuple[tuple[int, int], tuple[int, int]]]
|
|
320
|
-
A tuple specifying the bounds in nm for cropping the image before conversion,
|
|
321
|
-
formatted as ((min_x, min_y), (max_x, max_y)). If None, the entire image is
|
|
322
|
-
used.
|
|
323
|
-
**kwargs
|
|
324
|
-
Additional keyword arguments to be passed to the Device constructor.
|
|
325
|
-
|
|
326
|
-
Returns
|
|
327
|
-
-------
|
|
328
|
-
Device
|
|
329
|
-
A Device object representing the processed SEM image.
|
|
330
|
-
|
|
331
|
-
Raises
|
|
332
|
-
------
|
|
333
|
-
ValueError
|
|
334
|
-
If neither `sem_resolution` nor `sem_resolution_key` is provided.
|
|
335
|
-
"""
|
|
336
|
-
if sem_resolution is None and sem_resolution_key is not None:
|
|
337
|
-
sem_resolution = get_sem_resolution(sem_path, sem_resolution_key)
|
|
338
|
-
elif sem_resolution is None:
|
|
339
|
-
raise ValueError("Either sem_resolution or resolution_key must be provided.")
|
|
340
|
-
|
|
341
|
-
device_array = cv2.imread(sem_path, flags=cv2.IMREAD_GRAYSCALE)
|
|
342
|
-
device_array = cv2.resize(
|
|
343
|
-
device_array, dsize=(0, 0), fx=sem_resolution, fy=sem_resolution
|
|
344
|
-
)
|
|
345
|
-
|
|
346
|
-
if bounds is not None:
|
|
347
|
-
pad_left = max(0, -bounds[0][0])
|
|
348
|
-
pad_right = max(0, bounds[1][0] - device_array.shape[1])
|
|
349
|
-
pad_bottom = max(0, -bounds[0][1])
|
|
350
|
-
pad_top = max(0, bounds[1][1] - device_array.shape[0])
|
|
351
|
-
|
|
352
|
-
if pad_left or pad_right or pad_top or pad_bottom:
|
|
353
|
-
device_array = np.pad(
|
|
354
|
-
device_array,
|
|
355
|
-
((pad_top, pad_bottom), (pad_left, pad_right)),
|
|
356
|
-
mode="constant",
|
|
357
|
-
constant_values=0,
|
|
358
|
-
)
|
|
359
|
-
|
|
360
|
-
start_x = max(0, bounds[0][0] + pad_left)
|
|
361
|
-
end_x = min(device_array.shape[1], bounds[1][0] + pad_left)
|
|
362
|
-
start_y = max(0, device_array.shape[0] - (bounds[1][1] + pad_top))
|
|
363
|
-
end_y = min(
|
|
364
|
-
device_array.shape[0], device_array.shape[0] - (bounds[0][1] + pad_top)
|
|
365
|
-
)
|
|
366
|
-
|
|
367
|
-
if start_x >= end_x or start_y >= end_y:
|
|
368
|
-
raise ValueError(
|
|
369
|
-
"Invalid bounds resulted in zero-size array: "
|
|
370
|
-
f"x=[{start_x}, {end_x}], "
|
|
371
|
-
f"y=[{start_y}, {end_y}]"
|
|
372
|
-
)
|
|
373
|
-
|
|
374
|
-
device_array = device_array[start_y:end_y, start_x:end_x]
|
|
375
|
-
|
|
376
|
-
if binarize:
|
|
377
|
-
device_array = geometry.binarize_sem(device_array)
|
|
378
|
-
|
|
379
|
-
buffer_spec = BufferSpec(
|
|
380
|
-
mode={
|
|
381
|
-
"top": "none",
|
|
382
|
-
"bottom": "none",
|
|
383
|
-
"left": "none",
|
|
384
|
-
"right": "none",
|
|
385
|
-
},
|
|
386
|
-
thickness={
|
|
387
|
-
"top": 0,
|
|
388
|
-
"bottom": 0,
|
|
389
|
-
"left": 0,
|
|
390
|
-
"right": 0,
|
|
391
|
-
},
|
|
392
|
-
)
|
|
393
|
-
return Device(device_array=device_array, buffer_spec=buffer_spec, **kwargs)
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
def get_sem_resolution(sem_path: str, sem_resolution_key: str) -> float:
|
|
397
|
-
"""
|
|
398
|
-
Extracts the resolution of a scanning electron microscope (SEM) image from its
|
|
399
|
-
metadata.
|
|
400
|
-
|
|
401
|
-
Notes
|
|
402
|
-
-----
|
|
403
|
-
This function is used internally and may not be useful for most users.
|
|
404
|
-
|
|
405
|
-
Parameters
|
|
406
|
-
----------
|
|
407
|
-
sem_path : str
|
|
408
|
-
The file path to the SEM image.
|
|
409
|
-
sem_resolution_key : str
|
|
410
|
-
The key to look for in the SEM image metadata to extract the resolution.
|
|
411
|
-
|
|
412
|
-
Returns
|
|
413
|
-
-------
|
|
414
|
-
float
|
|
415
|
-
The resolution of the SEM image in nanometers per pixel.
|
|
416
|
-
|
|
417
|
-
Raises
|
|
418
|
-
------
|
|
419
|
-
ValueError
|
|
420
|
-
If the resolution key is not found in the SEM image metadata.
|
|
421
|
-
"""
|
|
422
|
-
with open(sem_path, "rb") as file:
|
|
423
|
-
resolution_key_bytes = sem_resolution_key.encode("utf-8")
|
|
424
|
-
for line in file:
|
|
425
|
-
if resolution_key_bytes in line:
|
|
426
|
-
line_str = line.decode("utf-8")
|
|
427
|
-
match = re.search(r"-?\d+(\.\d+)?", line_str)
|
|
428
|
-
if match:
|
|
429
|
-
value = float(match.group())
|
|
430
|
-
if value > 100:
|
|
431
|
-
value /= 1000
|
|
432
|
-
return value
|
|
433
|
-
raise ValueError(f"Resolution key '{sem_resolution_key}' not found in {sem_path}.")
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
def from_tidy3d(
|
|
437
|
-
tidy3d_sim: "td.Simulation",
|
|
438
|
-
eps: float,
|
|
439
|
-
z: float,
|
|
440
|
-
freq: float,
|
|
441
|
-
buffer_width: float = 0.1,
|
|
442
|
-
**kwargs,
|
|
443
|
-
) -> Device:
|
|
444
|
-
"""
|
|
445
|
-
Create a Device from a Tidy3D simulation.
|
|
446
|
-
|
|
447
|
-
Parameters
|
|
448
|
-
----------
|
|
449
|
-
tidy3d_sim : tidy3d.Simulation
|
|
450
|
-
The Tidy3D simulation object.
|
|
451
|
-
eps : float
|
|
452
|
-
The permittivity of the layer to extract from the simulation.
|
|
453
|
-
z : float
|
|
454
|
-
The z-coordinate of the layer to extract from the simulation.
|
|
455
|
-
freq : float
|
|
456
|
-
The frequency at which to extract the permittivity.
|
|
457
|
-
buffer_width : float
|
|
458
|
-
The width of the buffer region around the layer to extract from the
|
|
459
|
-
simulation. Defaults to 0.1 µm. This is useful for ensuring the inputs/outputs
|
|
460
|
-
of the simulation are not affected by prediction.
|
|
461
|
-
**kwargs
|
|
462
|
-
Additional keyword arguments to be passed to the Device constructor.
|
|
463
|
-
|
|
464
|
-
Returns
|
|
465
|
-
-------
|
|
466
|
-
Device
|
|
467
|
-
A Device object representing the permittivity cross-section at the specified
|
|
468
|
-
z-coordinate for the Tidy3D simulation.
|
|
469
|
-
|
|
470
|
-
Raises
|
|
471
|
-
------
|
|
472
|
-
ImportError
|
|
473
|
-
If the tidy3d package is not installed.
|
|
474
|
-
"""
|
|
475
|
-
try:
|
|
476
|
-
from tidy3d import Coords, Grid
|
|
477
|
-
except ImportError:
|
|
478
|
-
raise ImportError(
|
|
479
|
-
"The tidy3d package is required to use this function; "
|
|
480
|
-
"try `pip install tidy3d`."
|
|
481
|
-
) from None
|
|
482
|
-
|
|
483
|
-
X = np.arange(
|
|
484
|
-
tidy3d_sim.bounds[0][0] - buffer_width,
|
|
485
|
-
tidy3d_sim.bounds[1][0] + buffer_width,
|
|
486
|
-
0.001,
|
|
487
|
-
)
|
|
488
|
-
Y = np.arange(
|
|
489
|
-
tidy3d_sim.bounds[0][1] - buffer_width,
|
|
490
|
-
tidy3d_sim.bounds[1][1] + buffer_width,
|
|
491
|
-
0.001,
|
|
492
|
-
)
|
|
493
|
-
Z = np.array([z])
|
|
494
|
-
|
|
495
|
-
grid = Grid(attrs={}, boundaries=Coords(attrs={}, x=X, y=Y, z=Z))
|
|
496
|
-
eps_array = np.real(
|
|
497
|
-
tidy3d_sim.epsilon_on_grid(grid=grid, coord_key="boundaries", freq=freq).values
|
|
498
|
-
)
|
|
499
|
-
device_array = geometry.binarize_hard(device_array=eps_array, eta=eps - 0.1)[
|
|
500
|
-
:, :, 0
|
|
501
|
-
]
|
|
502
|
-
device_array = np.rot90(device_array, k=1)
|
|
503
|
-
return Device(device_array=device_array, **kwargs)
|