prefab 1.1.1__py3-none-any.whl → 1.1.3__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 +3 -2
- prefab/compare.py +17 -0
- prefab/device.py +40 -177
- prefab/geometry.py +43 -1
- prefab/models.py +1 -1
- prefab/predict.py +260 -0
- prefab/read.py +5 -1
- prefab/shapes.py +208 -201
- {prefab-1.1.1.dist-info → prefab-1.1.3.dist-info}/METADATA +12 -12
- prefab-1.1.3.dist-info/RECORD +13 -0
- prefab-1.1.1.dist-info/RECORD +0 -12
- {prefab-1.1.1.dist-info → prefab-1.1.3.dist-info}/WHEEL +0 -0
- {prefab-1.1.1.dist-info → prefab-1.1.3.dist-info}/licenses/LICENSE +0 -0
prefab/__init__.py
CHANGED
|
@@ -5,9 +5,9 @@ Usage:
|
|
|
5
5
|
import prefab as pf
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = "1.1.
|
|
8
|
+
__version__ = "1.1.3"
|
|
9
9
|
|
|
10
|
-
from . import compare, geometry, read, shapes
|
|
10
|
+
from . import compare, geometry, predict, read, shapes
|
|
11
11
|
from .device import BufferSpec, Device
|
|
12
12
|
from .models import models
|
|
13
13
|
|
|
@@ -15,6 +15,7 @@ __all__ = [
|
|
|
15
15
|
"Device",
|
|
16
16
|
"BufferSpec",
|
|
17
17
|
"geometry",
|
|
18
|
+
"predict",
|
|
18
19
|
"read",
|
|
19
20
|
"shapes",
|
|
20
21
|
"compare",
|
prefab/compare.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Provides functions to measure the similarity between devices."""
|
|
2
2
|
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
3
5
|
import numpy as np
|
|
4
6
|
|
|
5
7
|
from .device import Device
|
|
@@ -42,6 +44,11 @@ def intersection_over_union(device_a: Device, device_b: Device) -> float:
|
|
|
42
44
|
float
|
|
43
45
|
The Intersection over Union between two devices.
|
|
44
46
|
"""
|
|
47
|
+
if not device_a.is_binary or not device_b.is_binary:
|
|
48
|
+
warnings.warn(
|
|
49
|
+
"One or both devices are not binarized.", UserWarning, stacklevel=2
|
|
50
|
+
)
|
|
51
|
+
|
|
45
52
|
return np.sum(
|
|
46
53
|
np.logical_and(device_a.device_array, device_b.device_array)
|
|
47
54
|
) / np.sum(np.logical_or(device_a.device_array, device_b.device_array))
|
|
@@ -65,6 +72,11 @@ def hamming_distance(device_a: Device, device_b: Device) -> int:
|
|
|
65
72
|
int
|
|
66
73
|
The Hamming distance between two devices.
|
|
67
74
|
"""
|
|
75
|
+
if not device_a.is_binary or not device_b.is_binary:
|
|
76
|
+
warnings.warn(
|
|
77
|
+
"One or both devices are not binarized.", UserWarning, stacklevel=2
|
|
78
|
+
)
|
|
79
|
+
|
|
68
80
|
return np.sum(device_a.device_array != device_b.device_array)
|
|
69
81
|
|
|
70
82
|
|
|
@@ -86,6 +98,11 @@ def dice_coefficient(device_a: Device, device_b: Device) -> float:
|
|
|
86
98
|
float
|
|
87
99
|
The Dice coefficient between two devices.
|
|
88
100
|
"""
|
|
101
|
+
if not device_a.is_binary or not device_b.is_binary:
|
|
102
|
+
warnings.warn(
|
|
103
|
+
"One or both devices are not binarized.", UserWarning, stacklevel=2
|
|
104
|
+
)
|
|
105
|
+
|
|
89
106
|
intersection = 2.0 * np.sum(
|
|
90
107
|
np.logical_and(device_a.device_array, device_b.device_array)
|
|
91
108
|
)
|
prefab/device.py
CHANGED
|
@@ -1,28 +1,21 @@
|
|
|
1
1
|
"""Provides the Device class for representing photonic devices."""
|
|
2
2
|
|
|
3
|
-
import base64
|
|
4
|
-
import io
|
|
5
|
-
import json
|
|
6
|
-
import os
|
|
7
3
|
from typing import Optional
|
|
8
4
|
|
|
9
5
|
import cv2
|
|
10
6
|
import gdstk
|
|
11
7
|
import matplotlib.pyplot as plt
|
|
12
8
|
import numpy as np
|
|
13
|
-
import requests
|
|
14
|
-
import toml
|
|
15
9
|
from matplotlib.axes import Axes
|
|
16
10
|
from matplotlib.patches import Rectangle
|
|
17
11
|
from PIL import Image
|
|
18
12
|
from pydantic import BaseModel, Field, conint, root_validator, validator
|
|
19
13
|
from scipy.ndimage import distance_transform_edt
|
|
20
14
|
from skimage import measure
|
|
21
|
-
from skimage.morphology import closing, disk, opening, square
|
|
22
|
-
from tqdm import tqdm
|
|
23
15
|
|
|
24
16
|
from . import compare, geometry
|
|
25
17
|
from .models import Model
|
|
18
|
+
from .predict import predict_array
|
|
26
19
|
|
|
27
20
|
Image.MAX_IMAGE_PIXELS = None
|
|
28
21
|
|
|
@@ -193,6 +186,7 @@ class Device(BaseModel):
|
|
|
193
186
|
raise ValueError("device_array must be a 2D array.")
|
|
194
187
|
return values
|
|
195
188
|
|
|
189
|
+
@property
|
|
196
190
|
def is_binary(self) -> bool:
|
|
197
191
|
"""
|
|
198
192
|
Check if the device geometry is binary.
|
|
@@ -210,147 +204,6 @@ class Device(BaseModel):
|
|
|
210
204
|
or np.array_equal(unique_values, [1])
|
|
211
205
|
)
|
|
212
206
|
|
|
213
|
-
def _encode_array(self, array):
|
|
214
|
-
image = Image.fromarray(np.uint8(array * 255))
|
|
215
|
-
buffered = io.BytesIO()
|
|
216
|
-
image.save(buffered, format="PNG")
|
|
217
|
-
encoded_png = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
|
218
|
-
return encoded_png
|
|
219
|
-
|
|
220
|
-
def _decode_array(self, encoded_png):
|
|
221
|
-
binary_data = base64.b64decode(encoded_png)
|
|
222
|
-
image = Image.open(io.BytesIO(binary_data))
|
|
223
|
-
return np.array(image) / 255
|
|
224
|
-
|
|
225
|
-
def _predict_array(
|
|
226
|
-
self,
|
|
227
|
-
model: Model,
|
|
228
|
-
model_type: str,
|
|
229
|
-
binarize: bool,
|
|
230
|
-
gpu: bool = False,
|
|
231
|
-
) -> "Device":
|
|
232
|
-
try:
|
|
233
|
-
with open(os.path.expanduser("~/.prefab.toml")) as file:
|
|
234
|
-
content = file.readlines()
|
|
235
|
-
access_token = None
|
|
236
|
-
refresh_token = None
|
|
237
|
-
for line in content:
|
|
238
|
-
if "access_token" in line:
|
|
239
|
-
access_token = line.split("=")[1].strip().strip('"')
|
|
240
|
-
if "refresh_token" in line:
|
|
241
|
-
refresh_token = line.split("=")[1].strip().strip('"')
|
|
242
|
-
break
|
|
243
|
-
if not access_token or not refresh_token:
|
|
244
|
-
raise ValueError("Token not found in the configuration file.")
|
|
245
|
-
except FileNotFoundError:
|
|
246
|
-
raise FileNotFoundError(
|
|
247
|
-
"Could not validate user.\n"
|
|
248
|
-
"Please update prefab using: pip install --upgrade prefab.\n"
|
|
249
|
-
"Signup/login and generate a new token.\n"
|
|
250
|
-
"See https://www.prefabphotonics.com/docs/guides/quickstart."
|
|
251
|
-
) from None
|
|
252
|
-
|
|
253
|
-
headers = {
|
|
254
|
-
"Authorization": f"Bearer {access_token}",
|
|
255
|
-
"X-Refresh-Token": refresh_token,
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
predict_data = {
|
|
259
|
-
"device_array": self._encode_array(self.device_array[:, :, 0]),
|
|
260
|
-
"model": model.to_json(),
|
|
261
|
-
"model_type": model_type,
|
|
262
|
-
"binary": binarize,
|
|
263
|
-
}
|
|
264
|
-
json_data = json.dumps(predict_data)
|
|
265
|
-
|
|
266
|
-
endpoint_url = (
|
|
267
|
-
"https://prefab-photonics--predict-gpu-v1.modal.run"
|
|
268
|
-
if gpu
|
|
269
|
-
else "https://prefab-photonics--predict-v1.modal.run"
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
try:
|
|
273
|
-
with requests.post(
|
|
274
|
-
endpoint_url, data=json_data, headers=headers, stream=True
|
|
275
|
-
) as response:
|
|
276
|
-
response.raise_for_status()
|
|
277
|
-
event_type = None
|
|
278
|
-
model_descriptions = {
|
|
279
|
-
"p": "Prediction",
|
|
280
|
-
"c": "Correction",
|
|
281
|
-
"s": "SEMulate",
|
|
282
|
-
}
|
|
283
|
-
progress_bar = tqdm(
|
|
284
|
-
total=100,
|
|
285
|
-
desc=f"{model_descriptions[model_type]}",
|
|
286
|
-
unit="%",
|
|
287
|
-
colour="green",
|
|
288
|
-
bar_format="{l_bar}{bar:30}{r_bar}{bar:-10b}",
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
for line in response.iter_lines():
|
|
292
|
-
if line:
|
|
293
|
-
decoded_line = line.decode("utf-8").strip()
|
|
294
|
-
if decoded_line.startswith("event:"):
|
|
295
|
-
event_type = decoded_line.split(":")[1].strip()
|
|
296
|
-
elif decoded_line.startswith("data:"):
|
|
297
|
-
try:
|
|
298
|
-
data_content = json.loads(
|
|
299
|
-
decoded_line.split("data: ")[1]
|
|
300
|
-
)
|
|
301
|
-
if event_type == "progress":
|
|
302
|
-
progress = round(100 * data_content["progress"])
|
|
303
|
-
progress_bar.update(progress - progress_bar.n)
|
|
304
|
-
elif event_type == "result":
|
|
305
|
-
results = []
|
|
306
|
-
for key in sorted(data_content.keys()):
|
|
307
|
-
if key.startswith("result"):
|
|
308
|
-
decoded_image = self._decode_array(
|
|
309
|
-
data_content[key]
|
|
310
|
-
)
|
|
311
|
-
results.append(decoded_image)
|
|
312
|
-
|
|
313
|
-
if results:
|
|
314
|
-
prediction = np.stack(results, axis=-1)
|
|
315
|
-
if binarize:
|
|
316
|
-
prediction = geometry.binarize_hard(
|
|
317
|
-
prediction
|
|
318
|
-
)
|
|
319
|
-
progress_bar.close()
|
|
320
|
-
return prediction
|
|
321
|
-
elif event_type == "end":
|
|
322
|
-
print("Stream ended.")
|
|
323
|
-
progress_bar.close()
|
|
324
|
-
break
|
|
325
|
-
elif event_type == "auth":
|
|
326
|
-
if "new_refresh_token" in data_content["auth"]:
|
|
327
|
-
prefab_file_path = os.path.expanduser(
|
|
328
|
-
"~/.prefab.toml"
|
|
329
|
-
)
|
|
330
|
-
with open(
|
|
331
|
-
prefab_file_path, "w", encoding="utf-8"
|
|
332
|
-
) as toml_file:
|
|
333
|
-
toml.dump(
|
|
334
|
-
{
|
|
335
|
-
"access_token": data_content[
|
|
336
|
-
"auth"
|
|
337
|
-
]["new_access_token"],
|
|
338
|
-
"refresh_token": data_content[
|
|
339
|
-
"auth"
|
|
340
|
-
]["new_refresh_token"],
|
|
341
|
-
},
|
|
342
|
-
toml_file,
|
|
343
|
-
)
|
|
344
|
-
elif event_type == "error":
|
|
345
|
-
raise ValueError(f"{data_content['error']}")
|
|
346
|
-
except json.JSONDecodeError:
|
|
347
|
-
raise ValueError(
|
|
348
|
-
"Failed to decode JSON:",
|
|
349
|
-
decoded_line.split("data: ")[1],
|
|
350
|
-
) from None
|
|
351
|
-
except requests.RequestException as e:
|
|
352
|
-
raise RuntimeError(f"Request failed: {e}") from e
|
|
353
|
-
|
|
354
207
|
def predict(
|
|
355
208
|
self,
|
|
356
209
|
model: Model,
|
|
@@ -393,7 +246,8 @@ class Device(BaseModel):
|
|
|
393
246
|
If the prediction service returns an error or if the response from the
|
|
394
247
|
service cannot be processed correctly.
|
|
395
248
|
"""
|
|
396
|
-
prediction_array =
|
|
249
|
+
prediction_array = predict_array(
|
|
250
|
+
device_array=self.device_array,
|
|
397
251
|
model=model,
|
|
398
252
|
model_type="p",
|
|
399
253
|
binarize=binarize,
|
|
@@ -445,7 +299,8 @@ class Device(BaseModel):
|
|
|
445
299
|
If the correction service returns an error or if the response from the
|
|
446
300
|
service cannot be processed correctly.
|
|
447
301
|
"""
|
|
448
|
-
correction_array =
|
|
302
|
+
correction_array = predict_array(
|
|
303
|
+
device_array=self.device_array,
|
|
449
304
|
model=model,
|
|
450
305
|
model_type="c",
|
|
451
306
|
binarize=binarize,
|
|
@@ -487,7 +342,8 @@ class Device(BaseModel):
|
|
|
487
342
|
A new instance of the Device class with its geometry transformed to simulate
|
|
488
343
|
an SEM image style.
|
|
489
344
|
"""
|
|
490
|
-
semulated_array =
|
|
345
|
+
semulated_array = predict_array(
|
|
346
|
+
device_array=self.device_array,
|
|
491
347
|
model=model,
|
|
492
348
|
model_type="s",
|
|
493
349
|
binarize=False,
|
|
@@ -550,6 +406,7 @@ class Device(BaseModel):
|
|
|
550
406
|
cell_name: str = "prefab_device",
|
|
551
407
|
gds_layer: tuple[int, int] = (1, 0),
|
|
552
408
|
contour_approx_mode: int = 2,
|
|
409
|
+
origin: tuple[float, float] = (0.0, 0.0),
|
|
553
410
|
):
|
|
554
411
|
"""
|
|
555
412
|
Exports the device geometry as a GDSII file.
|
|
@@ -572,11 +429,15 @@ class Device(BaseModel):
|
|
|
572
429
|
The mode of contour approximation used during the conversion. Defaults to 2,
|
|
573
430
|
which corresponds to `cv2.CHAIN_APPROX_SIMPLE`, a method that compresses
|
|
574
431
|
horizontal, vertical, and diagonal segments and leaves only their endpoints.
|
|
432
|
+
origin : tuple[float, float], optional
|
|
433
|
+
The x and y coordinates of the origin for the GDSII export. Defaults to
|
|
434
|
+
(0.0, 0.0).
|
|
575
435
|
"""
|
|
576
436
|
gdstk_cell = self.flatten()._device_to_gdstk(
|
|
577
437
|
cell_name=cell_name,
|
|
578
438
|
gds_layer=gds_layer,
|
|
579
439
|
contour_approx_mode=contour_approx_mode,
|
|
440
|
+
origin=origin,
|
|
580
441
|
)
|
|
581
442
|
print(f"Saving GDS to '{gds_path}'...")
|
|
582
443
|
gdstk_library = gdstk.Library()
|
|
@@ -588,6 +449,7 @@ class Device(BaseModel):
|
|
|
588
449
|
cell_name: str = "prefab_device",
|
|
589
450
|
gds_layer: tuple[int, int] = (1, 0),
|
|
590
451
|
contour_approx_mode: int = 2,
|
|
452
|
+
origin: tuple[float, float] = (0.0, 0.0),
|
|
591
453
|
):
|
|
592
454
|
"""
|
|
593
455
|
Converts the device geometry to a GDSTK cell object.
|
|
@@ -607,6 +469,9 @@ class Device(BaseModel):
|
|
|
607
469
|
The mode of contour approximation used during the conversion. Defaults to 2,
|
|
608
470
|
which corresponds to `cv2.CHAIN_APPROX_SIMPLE`, a method that compresses
|
|
609
471
|
horizontal, vertical, and diagonal segments and leaves only their endpoints.
|
|
472
|
+
origin : tuple[float, float], optional
|
|
473
|
+
The x and y coordinates of the origin for the GDSTK cell. Defaults to
|
|
474
|
+
(0.0, 0.0).
|
|
610
475
|
|
|
611
476
|
Returns
|
|
612
477
|
-------
|
|
@@ -618,6 +483,7 @@ class Device(BaseModel):
|
|
|
618
483
|
cell_name=cell_name,
|
|
619
484
|
gds_layer=gds_layer,
|
|
620
485
|
contour_approx_mode=contour_approx_mode,
|
|
486
|
+
origin=origin,
|
|
621
487
|
)
|
|
622
488
|
return gdstk_cell
|
|
623
489
|
|
|
@@ -626,6 +492,7 @@ class Device(BaseModel):
|
|
|
626
492
|
cell_name: str,
|
|
627
493
|
gds_layer: tuple[int, int],
|
|
628
494
|
contour_approx_mode: int,
|
|
495
|
+
origin: tuple[float, float],
|
|
629
496
|
) -> gdstk.Cell:
|
|
630
497
|
approx_mode_mapping = {
|
|
631
498
|
1: cv2.CHAIN_APPROX_NONE,
|
|
@@ -662,8 +529,21 @@ class Device(BaseModel):
|
|
|
662
529
|
polygons_to_process = hierarchy_polygons[level]
|
|
663
530
|
|
|
664
531
|
if polygons_to_process:
|
|
532
|
+
center_x_nm = self.device_array.shape[1] / 2
|
|
533
|
+
center_y_nm = self.device_array.shape[0] / 2
|
|
534
|
+
|
|
535
|
+
center_x_um = center_x_nm / 1000
|
|
536
|
+
center_y_um = center_y_nm / 1000
|
|
537
|
+
|
|
538
|
+
adjusted_polygons = [
|
|
539
|
+
[
|
|
540
|
+
(x - center_x_um + origin[0], y - center_y_um + origin[1])
|
|
541
|
+
for x, y in polygon
|
|
542
|
+
]
|
|
543
|
+
for polygon in polygons_to_process
|
|
544
|
+
]
|
|
665
545
|
processed_polygons = gdstk.boolean(
|
|
666
|
-
|
|
546
|
+
adjusted_polygons,
|
|
667
547
|
processed_polygons,
|
|
668
548
|
operation,
|
|
669
549
|
layer=gds_layer[0],
|
|
@@ -1313,13 +1193,6 @@ class Device(BaseModel):
|
|
|
1313
1193
|
"""
|
|
1314
1194
|
Trim the device geometry by removing empty space around it.
|
|
1315
1195
|
|
|
1316
|
-
Parameters
|
|
1317
|
-
----------
|
|
1318
|
-
buffer_thickness : dict, optional
|
|
1319
|
-
A dictionary specifying the thickness of the buffer to leave around the
|
|
1320
|
-
non-zero elements of the array. Should contain keys 'top', 'bottom', 'left',
|
|
1321
|
-
'right'. Defaults to None, which means no buffer is added.
|
|
1322
|
-
|
|
1323
1196
|
Returns
|
|
1324
1197
|
-------
|
|
1325
1198
|
Device
|
|
@@ -1414,15 +1287,10 @@ class Device(BaseModel):
|
|
|
1414
1287
|
Flatten the device geometry by summing the vertical layers and normalizing the
|
|
1415
1288
|
result.
|
|
1416
1289
|
|
|
1417
|
-
Parameters
|
|
1418
|
-
----------
|
|
1419
|
-
device_array : np.ndarray
|
|
1420
|
-
The input array to be flattened.
|
|
1421
|
-
|
|
1422
1290
|
Returns
|
|
1423
1291
|
-------
|
|
1424
|
-
|
|
1425
|
-
|
|
1292
|
+
Device
|
|
1293
|
+
A new instance of the Device with the flattened geometry.
|
|
1426
1294
|
"""
|
|
1427
1295
|
flattened_device_array = geometry.flatten(device_array=self.device_array)
|
|
1428
1296
|
return self.model_copy(update={"device_array": flattened_device_array})
|
|
@@ -1472,16 +1340,11 @@ class Device(BaseModel):
|
|
|
1472
1340
|
ValueError
|
|
1473
1341
|
If an invalid structuring element type is specified.
|
|
1474
1342
|
"""
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
raise ValueError(f"Invalid structuring element: {strel}")
|
|
1481
|
-
|
|
1482
|
-
modified_geometry = closing(self.device_array[:, :, 0], structuring_element)
|
|
1483
|
-
modified_geometry = opening(modified_geometry, structuring_element)
|
|
1484
|
-
modified_geometry = np.expand_dims(modified_geometry, axis=-1)
|
|
1343
|
+
modified_geometry = geometry.enforce_feature_size(
|
|
1344
|
+
device_array=self.device_array,
|
|
1345
|
+
min_feature_size=min_feature_size,
|
|
1346
|
+
strel=strel,
|
|
1347
|
+
)
|
|
1485
1348
|
return self.model_copy(update={"device_array": modified_geometry})
|
|
1486
1349
|
|
|
1487
1350
|
def check_feature_size(self, min_feature_size: int, strel: str = "disk"):
|
prefab/geometry.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import cv2
|
|
4
4
|
import numpy as np
|
|
5
|
+
from skimage.morphology import closing, disk, opening, square
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def normalize(device_array: np.ndarray) -> np.ndarray:
|
|
@@ -248,7 +249,6 @@ def rotate(device_array: np.ndarray, angle: float) -> np.ndarray:
|
|
|
248
249
|
),
|
|
249
250
|
axis=-1,
|
|
250
251
|
)
|
|
251
|
-
return np.expand_dims(rotated_device_array, axis=-1)
|
|
252
252
|
|
|
253
253
|
|
|
254
254
|
def erode(device_array: np.ndarray, kernel_size: int) -> np.ndarray:
|
|
@@ -306,3 +306,45 @@ def flatten(device_array: np.ndarray) -> np.ndarray:
|
|
|
306
306
|
The flattened array with values scaled between 0 and 1.
|
|
307
307
|
"""
|
|
308
308
|
return normalize(np.sum(device_array, axis=-1, keepdims=True))
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def enforce_feature_size(
|
|
312
|
+
device_array: np.ndarray, min_feature_size: int, strel: str = "disk"
|
|
313
|
+
) -> np.ndarray:
|
|
314
|
+
"""
|
|
315
|
+
Enforce a minimum feature size on the device geometry.
|
|
316
|
+
|
|
317
|
+
This function applies morphological operations to ensure that all features in the
|
|
318
|
+
device geometry are at least the specified minimum size. It uses either a disk
|
|
319
|
+
or square structuring element for the operations.
|
|
320
|
+
|
|
321
|
+
Parameters
|
|
322
|
+
----------
|
|
323
|
+
device_array : np.ndarray
|
|
324
|
+
The input array representing the device geometry.
|
|
325
|
+
min_feature_size : int
|
|
326
|
+
The minimum feature size to enforce, in nanometers.
|
|
327
|
+
strel : str, optional
|
|
328
|
+
The type of structuring element to use. Can be either "disk" or "square".
|
|
329
|
+
Defaults to "disk".
|
|
330
|
+
|
|
331
|
+
Returns
|
|
332
|
+
-------
|
|
333
|
+
np.ndarray
|
|
334
|
+
The modified device array with enforced feature size.
|
|
335
|
+
|
|
336
|
+
Raises
|
|
337
|
+
------
|
|
338
|
+
ValueError
|
|
339
|
+
If an invalid structuring element type is specified.
|
|
340
|
+
"""
|
|
341
|
+
if strel == "disk":
|
|
342
|
+
structuring_element = disk(radius=min_feature_size / 2)
|
|
343
|
+
elif strel == "square":
|
|
344
|
+
structuring_element = square(width=min_feature_size)
|
|
345
|
+
else:
|
|
346
|
+
raise ValueError(f"Invalid structuring element: {strel}")
|
|
347
|
+
|
|
348
|
+
modified_geometry = closing(device_array[:, :, 0], structuring_element)
|
|
349
|
+
modified_geometry = opening(modified_geometry, structuring_element)
|
|
350
|
+
return np.expand_dims(modified_geometry, axis=-1)
|