openuv 1.0.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.
- euv/__init__.py +19 -0
- euv/accel/__init__.py +25 -0
- euv/accel/chunked.py +124 -0
- euv/accel/device.py +88 -0
- euv/accel/mixed_precision.py +102 -0
- euv/accel/vram_budget.py +211 -0
- euv/aerial/__init__.py +20 -0
- euv/aerial/abbe.py +343 -0
- euv/aerial/hopkins.py +330 -0
- euv/aerial/pupil.py +206 -0
- euv/aerial/source.py +193 -0
- euv/api/__init__.py +11 -0
- euv/api/main.py +257 -0
- euv/api/schemas.py +171 -0
- euv/api/static/app.js +289 -0
- euv/api/static/index.html +164 -0
- euv/api/static/simulate.html +688 -0
- euv/api/static/style.css +533 -0
- euv/calibrate/__init__.py +58 -0
- euv/calibrate/wafer_fit.py +455 -0
- euv/constants.py +108 -0
- euv/data/cxro/Ac.csv +517 -0
- euv/data/cxro/Ag.csv +509 -0
- euv/data/cxro/Al.csv +505 -0
- euv/data/cxro/Ar.csv +507 -0
- euv/data/cxro/As.csv +507 -0
- euv/data/cxro/At.csv +517 -0
- euv/data/cxro/Au.csv +507 -0
- euv/data/cxro/B.csv +503 -0
- euv/data/cxro/Ba.csv +509 -0
- euv/data/cxro/Be.csv +725 -0
- euv/data/cxro/Bi.csv +517 -0
- euv/data/cxro/Br.csv +507 -0
- euv/data/cxro/C.csv +503 -0
- euv/data/cxro/Ca.csv +505 -0
- euv/data/cxro/Cd.csv +511 -0
- euv/data/cxro/Ce.csv +509 -0
- euv/data/cxro/Cl.csv +505 -0
- euv/data/cxro/Co.csv +505 -0
- euv/data/cxro/Cr.csv +752 -0
- euv/data/cxro/Cs.csv +509 -0
- euv/data/cxro/Cu.csv +505 -0
- euv/data/cxro/Dy.csv +515 -0
- euv/data/cxro/Er.csv +515 -0
- euv/data/cxro/Eu.csv +515 -0
- euv/data/cxro/F.csv +503 -0
- euv/data/cxro/Fe.csv +505 -0
- euv/data/cxro/Fr.csv +517 -0
- euv/data/cxro/Ga.csv +507 -0
- euv/data/cxro/Gd.csv +720 -0
- euv/data/cxro/Ge.csv +507 -0
- euv/data/cxro/H.csv +502 -0
- euv/data/cxro/He.csv +502 -0
- euv/data/cxro/Hf.csv +515 -0
- euv/data/cxro/Hg.csv +517 -0
- euv/data/cxro/Ho.csv +515 -0
- euv/data/cxro/I.csv +509 -0
- euv/data/cxro/In.csv +511 -0
- euv/data/cxro/Ir.csv +516 -0
- euv/data/cxro/K.csv +504 -0
- euv/data/cxro/Kr.csv +510 -0
- euv/data/cxro/La.csv +712 -0
- euv/data/cxro/Li.csv +502 -0
- euv/data/cxro/Lu.csv +515 -0
- euv/data/cxro/Mg.csv +756 -0
- euv/data/cxro/Mn.csv +505 -0
- euv/data/cxro/Mo.csv +910 -0
- euv/data/cxro/N.csv +504 -0
- euv/data/cxro/Na.csv +505 -0
- euv/data/cxro/Nb.csv +758 -0
- euv/data/cxro/Nd.csv +509 -0
- euv/data/cxro/Ne.csv +504 -0
- euv/data/cxro/Ni.csv +505 -0
- euv/data/cxro/O.csv +503 -0
- euv/data/cxro/Os.csv +517 -0
- euv/data/cxro/P.csv +505 -0
- euv/data/cxro/Pa.csv +517 -0
- euv/data/cxro/Pb.csv +517 -0
- euv/data/cxro/Pd.csv +508 -0
- euv/data/cxro/Pm.csv +509 -0
- euv/data/cxro/Po.csv +517 -0
- euv/data/cxro/Pr.csv +509 -0
- euv/data/cxro/Pt.csv +946 -0
- euv/data/cxro/Ra.csv +517 -0
- euv/data/cxro/Rb.csv +509 -0
- euv/data/cxro/Re.csv +517 -0
- euv/data/cxro/Rh.csv +509 -0
- euv/data/cxro/Rn.csv +517 -0
- euv/data/cxro/Ru.csv +616 -0
- euv/data/cxro/S.csv +505 -0
- euv/data/cxro/Sb.csv +509 -0
- euv/data/cxro/Sc.csv +599 -0
- euv/data/cxro/Se.csv +507 -0
- euv/data/cxro/Si.csv +757 -0
- euv/data/cxro/Sm.csv +509 -0
- euv/data/cxro/Sn.csv +511 -0
- euv/data/cxro/Sr.csv +509 -0
- euv/data/cxro/Ta.csv +717 -0
- euv/data/cxro/Tb.csv +515 -0
- euv/data/cxro/Tc.csv +509 -0
- euv/data/cxro/Te.csv +509 -0
- euv/data/cxro/Th.csv +517 -0
- euv/data/cxro/Ti.csv +531 -0
- euv/data/cxro/Tl.csv +517 -0
- euv/data/cxro/Tm.csv +515 -0
- euv/data/cxro/U.csv +515 -0
- euv/data/cxro/V.csv +505 -0
- euv/data/cxro/W.csv +542 -0
- euv/data/cxro/Xe.csv +510 -0
- euv/data/cxro/Y.csv +620 -0
- euv/data/cxro/Yb.csv +515 -0
- euv/data/cxro/Zn.csv +505 -0
- euv/data/cxro/Zr.csv +725 -0
- euv/etch/__init__.py +61 -0
- euv/etch/bias.py +422 -0
- euv/io/__init__.py +33 -0
- euv/io/cli.py +426 -0
- euv/io/gds.py +330 -0
- euv/io/rasterize.py +189 -0
- euv/mask3d/__init__.py +16 -0
- euv/mask3d/geometry.py +208 -0
- euv/mask3d/rcwa2d.py +517 -0
- euv/mask3d/rcwa_torch.py +442 -0
- euv/materials.py +424 -0
- euv/metro/__init__.py +74 -0
- euv/metro/cd.py +480 -0
- euv/metro/process_window.py +312 -0
- euv/metro/sem_render.py +279 -0
- euv/opc/__init__.py +13 -0
- euv/opc/openilt_bridge.py +235 -0
- euv/optics/__init__.py +13 -0
- euv/optics/collector.py +184 -0
- euv/optics/multilayer.py +315 -0
- euv/optics/plot.py +306 -0
- euv/optics/tmm.py +411 -0
- euv/pipeline.py +356 -0
- euv/resist/__init__.py +67 -0
- euv/resist/develop.py +375 -0
- euv/resist/exposure.py +278 -0
- euv/resist/peb.py +372 -0
- euv/resist/stochastic.py +642 -0
- euv/source/__init__.py +27 -0
- euv/source/plasma.py +346 -0
- euv/source/spectrum.py +210 -0
- openuv-1.0.0.dist-info/METADATA +267 -0
- openuv-1.0.0.dist-info/RECORD +150 -0
- openuv-1.0.0.dist-info/WHEEL +5 -0
- openuv-1.0.0.dist-info/entry_points.txt +2 -0
- openuv-1.0.0.dist-info/licenses/LICENSE +201 -0
- openuv-1.0.0.dist-info/top_level.txt +1 -0
euv/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""euv — Open Source Extreme Ultraviolet Lithography Simulator
|
|
2
|
+
=============================================================
|
|
3
|
+
|
|
4
|
+
OpEnUV (Open Source EUV) is a modular, GPU-accelerated simulator for
|
|
5
|
+
extreme ultraviolet (EUV) lithography at 13.5 nm wavelength.
|
|
6
|
+
|
|
7
|
+
Key capabilities:
|
|
8
|
+
• Rigorous coupled-wave analysis (RCWA) for mask-3D effects
|
|
9
|
+
• Transfer-matrix method (TMM) for multilayer mirror optics
|
|
10
|
+
• Abbe/Hopkins partially coherent imaging at NA 0.33 / 0.55
|
|
11
|
+
• LPP tin-plasma source model (literature-fitted)
|
|
12
|
+
• Stochastic resist simulation (CAR/MOR, secondary-electron cascade)
|
|
13
|
+
• GDSII/OASIS layout import/export
|
|
14
|
+
• REST API and Python SDK for pipeline integration
|
|
15
|
+
|
|
16
|
+
License: Apache-2.0
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
__version__ = "1.0.0"
|
euv/accel/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""GPU Acceleration Layer — device management, VRAM budgeting, chunked
|
|
2
|
+
processing, and mixed-precision helpers for OpEnUV.
|
|
3
|
+
|
|
4
|
+
The ``accel`` package decouples simulation code from hardware details,
|
|
5
|
+
providing transparent fallback between CPU and GPU, memory-aware
|
|
6
|
+
workload sizing, and precision policies that maximise throughput on
|
|
7
|
+
consumer GPUs.
|
|
8
|
+
|
|
9
|
+
Submodules
|
|
10
|
+
----------
|
|
11
|
+
device
|
|
12
|
+
Device selection, introspection, and default dtype management.
|
|
13
|
+
vram_budget
|
|
14
|
+
Analytical VRAM estimators for RCWA and Abbe imaging, plus OOM
|
|
15
|
+
pre-checks and a human-readable budget report.
|
|
16
|
+
chunked
|
|
17
|
+
Chunked (source-point) Abbe summation and layer-wise RCWA for
|
|
18
|
+
fitting large problems into available memory.
|
|
19
|
+
mixed_precision
|
|
20
|
+
Context-aware dtype switching: ``complex64`` vs ``complex128``,
|
|
21
|
+
``float32`` vs ``float64``, with per-operation policy
|
|
22
|
+
recommendations.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
euv/accel/chunked.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Chunked processing kernels for GPU memory-limited simulations.
|
|
2
|
+
|
|
3
|
+
Processes large problems — many source points (Abbe) or many RCWA
|
|
4
|
+
layers — in batches, accumulating intermediate results on the GPU
|
|
5
|
+
without exceeding the VRAM budget.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import torch
|
|
11
|
+
|
|
12
|
+
from euv.aerial.abbe import abbe_image
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def chunked_abbe(
|
|
16
|
+
mask_fft: torch.Tensor,
|
|
17
|
+
source_points: torch.Tensor,
|
|
18
|
+
fx: torch.Tensor,
|
|
19
|
+
fy: torch.Tensor,
|
|
20
|
+
pupil: torch.Tensor,
|
|
21
|
+
na: float = 0.33,
|
|
22
|
+
chunk_size: int = 32,
|
|
23
|
+
period_m: float = 64e-9,
|
|
24
|
+
wavelength_m: float = 13.5e-9,
|
|
25
|
+
) -> torch.Tensor:
|
|
26
|
+
"""Compute the aerial image via chunked Abbe summation.
|
|
27
|
+
|
|
28
|
+
Iterates over source points in chunks of *chunk_size*, computing
|
|
29
|
+
the partial coherent image for each chunk and accumulating into
|
|
30
|
+
the final result. This trades a small loop overhead for a
|
|
31
|
+
significantly reduced peak GPU memory footprint.
|
|
32
|
+
|
|
33
|
+
For each chunk, the source tensor is zeroed everywhere except for
|
|
34
|
+
the chunk's source points, and ``abbe_image`` runs on that masked
|
|
35
|
+
source. Results are summed across chunks.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
mask_fft : (G, G) complex128
|
|
40
|
+
2D FFT of the mask transmission.
|
|
41
|
+
source_points : (Sx, Sy) float64
|
|
42
|
+
Full illumination source intensity distribution.
|
|
43
|
+
fx, fy : (G, G) float64
|
|
44
|
+
Normalised frequency coordinates from ``pupil_grid()``.
|
|
45
|
+
pupil : (G, G) complex128 or float64
|
|
46
|
+
Pupil transmission function.
|
|
47
|
+
na : float
|
|
48
|
+
Numerical aperture.
|
|
49
|
+
chunk_size : int
|
|
50
|
+
Maximum number of source points to process per chunk
|
|
51
|
+
(default 32). Lower values reduce peak memory but increase
|
|
52
|
+
Python loop overhead.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
aerial : (G, G) float64
|
|
57
|
+
Normalised aerial image intensity, matching the output of
|
|
58
|
+
``abbe_image`` run on the full source (within numerical
|
|
59
|
+
precision).
|
|
60
|
+
"""
|
|
61
|
+
G = mask_fft.shape[0]
|
|
62
|
+
device = mask_fft.device
|
|
63
|
+
dtype = source_points.dtype
|
|
64
|
+
|
|
65
|
+
# Identify all non-zero source point indices
|
|
66
|
+
src_mask = source_points > 0
|
|
67
|
+
src_indices = torch.nonzero(src_mask) # (n_src, 2)
|
|
68
|
+
n_src = src_indices.shape[0]
|
|
69
|
+
|
|
70
|
+
if n_src == 0:
|
|
71
|
+
return torch.zeros(G, G, dtype=torch.float64, device=device)
|
|
72
|
+
|
|
73
|
+
# Extract source weights at non-zero positions
|
|
74
|
+
weights = source_points[src_mask] # (n_src,)
|
|
75
|
+
|
|
76
|
+
aerial = torch.zeros(G, G, dtype=torch.float64, device=device)
|
|
77
|
+
|
|
78
|
+
for start in range(0, n_src, chunk_size):
|
|
79
|
+
end = min(start + chunk_size, n_src)
|
|
80
|
+
chunk_indices = src_indices[start:end]
|
|
81
|
+
|
|
82
|
+
# Build a source tensor that is zero everywhere except the
|
|
83
|
+
# chunk's source points. This is the simplest approach and
|
|
84
|
+
# matches the abbe_image interface exactly.
|
|
85
|
+
chunk_source = torch.zeros_like(source_points)
|
|
86
|
+
# Set the chunk's source points
|
|
87
|
+
chunk_source[chunk_indices[:, 0], chunk_indices[:, 1]] = weights[start:end]
|
|
88
|
+
|
|
89
|
+
# Compute partial aerial image for this chunk
|
|
90
|
+
partial = abbe_image(mask_fft, chunk_source, fx, fy, pupil, na, period_m=period_m, wavelength_m=wavelength_m)
|
|
91
|
+
aerial = aerial + partial
|
|
92
|
+
|
|
93
|
+
return aerial
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def chunked_rcwa(
|
|
97
|
+
geometry_profile: torch.Tensor,
|
|
98
|
+
chunk_size_mb: int = 500,
|
|
99
|
+
) -> torch.Tensor:
|
|
100
|
+
"""Process RCWA layers in chunks to stay within a memory budget.
|
|
101
|
+
|
|
102
|
+
.. note::
|
|
103
|
+
|
|
104
|
+
This is a placeholder implementation. The full RCWA chunked
|
|
105
|
+
solver will cascade per-layer S-matrices in groups rather than
|
|
106
|
+
all at once. Currently returns the input unchanged to
|
|
107
|
+
validate the API contract.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
geometry_profile : (N, ...) complex128
|
|
112
|
+
Permittivity profile through the mask stack, with the first
|
|
113
|
+
dimension indexing the vertical layer.
|
|
114
|
+
chunk_size_mb : int
|
|
115
|
+
Target memory budget per chunk in megabytes (default 500).
|
|
116
|
+
|
|
117
|
+
Returns
|
|
118
|
+
-------
|
|
119
|
+
torch.Tensor
|
|
120
|
+
The input profile (placeholder — full chunked RCWA to be
|
|
121
|
+
implemented in a future release).
|
|
122
|
+
"""
|
|
123
|
+
_ = chunk_size_mb # reserved for future use
|
|
124
|
+
return geometry_profile
|
euv/accel/device.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Device selection, introspection, and default dtype management.
|
|
2
|
+
|
|
3
|
+
Provides a single source of truth for which hardware (CPU / CUDA GPU)
|
|
4
|
+
simulation kernels run on, and what precision the tensor operations
|
|
5
|
+
use by default.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Dict
|
|
11
|
+
|
|
12
|
+
import torch
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def select_device(prefer_gpu: bool = True) -> torch.device:
|
|
16
|
+
"""Return the best available device.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
prefer_gpu : bool
|
|
21
|
+
If ``True`` (default) and a CUDA-capable GPU is available,
|
|
22
|
+
returns ``device(type='cuda')``. Otherwise returns
|
|
23
|
+
``device(type='cpu')``.
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
torch.device
|
|
28
|
+
"""
|
|
29
|
+
if prefer_gpu and torch.cuda.is_available():
|
|
30
|
+
return torch.device("cuda")
|
|
31
|
+
return torch.device("cpu")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def device_info(device: torch.device) -> Dict[str, object]:
|
|
35
|
+
"""Return a dictionary of hardware properties for *device*.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
device : torch.device
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
dict
|
|
44
|
+
Keys:
|
|
45
|
+
- ``name`` — device name string (e.g. ``"NVIDIA A100"`` or ``"cpu"``).
|
|
46
|
+
- ``vram_gb`` — total GPU memory in GiB, or 0.0 for CPU.
|
|
47
|
+
- ``compute_capability`` — CUDA compute capability ``(major, minor)``
|
|
48
|
+
tuple, or ``None`` for CPU.
|
|
49
|
+
"""
|
|
50
|
+
if device.type == "cuda":
|
|
51
|
+
idx = device.index if device.index is not None else 0
|
|
52
|
+
name = torch.cuda.get_device_name(idx)
|
|
53
|
+
vram_gb = torch.cuda.get_device_properties(idx).total_mem / (1024**3)
|
|
54
|
+
cap = torch.cuda.get_device_capability(idx)
|
|
55
|
+
return {
|
|
56
|
+
"name": name,
|
|
57
|
+
"vram_gb": round(vram_gb, 2),
|
|
58
|
+
"compute_capability": cap,
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
"name": "cpu",
|
|
62
|
+
"vram_gb": 0.0,
|
|
63
|
+
"compute_capability": None,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def set_default_dtype(
|
|
68
|
+
complex_dtype: torch.dtype = torch.complex128,
|
|
69
|
+
real_dtype: torch.dtype = torch.float64,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Set PyTorch's default floating-point and complex dtypes.
|
|
72
|
+
|
|
73
|
+
Call this once at the start of a simulation to establish the
|
|
74
|
+
precision regime for the entire module.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
complex_dtype : torch.dtype
|
|
79
|
+
Default complex type (e.g. ``torch.complex128`` or
|
|
80
|
+
``torch.complex64``).
|
|
81
|
+
real_dtype : torch.dtype
|
|
82
|
+
Default real type (e.g. ``torch.float64`` or ``torch.float32``).
|
|
83
|
+
"""
|
|
84
|
+
torch.set_default_dtype(real_dtype)
|
|
85
|
+
# torch does not have a global set_default_complex_dtype in older
|
|
86
|
+
# versions; instead we ensure the default dtype is set correctly
|
|
87
|
+
# and rely on complex tensors being constructed explicitly.
|
|
88
|
+
_ = complex_dtype # kept for API symmetry / future use
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Mixed-precision helpers for GPU memory reduction.
|
|
2
|
+
|
|
3
|
+
Provides dtype conversion utilities and a policy engine that
|
|
4
|
+
recommends the appropriate precision for each operation based on
|
|
5
|
+
the available VRAM.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Dict
|
|
11
|
+
|
|
12
|
+
import torch
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def autocast_complex(tensor: torch.Tensor) -> torch.Tensor:
|
|
16
|
+
"""Downcast a complex tensor to ``complex64`` if it is currently
|
|
17
|
+
``complex128`` and the precision loss is acceptable.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
tensor : torch.Tensor
|
|
22
|
+
Input tensor. Passed through unchanged if it is not complex
|
|
23
|
+
or already ``complex64``.
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
torch.Tensor
|
|
28
|
+
``complex64`` tensor if the input was ``complex128``,
|
|
29
|
+
otherwise the original tensor.
|
|
30
|
+
"""
|
|
31
|
+
if tensor.is_complex() and tensor.dtype == torch.complex128:
|
|
32
|
+
return tensor.to(torch.complex64)
|
|
33
|
+
return tensor
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def real_only(complex_tensor: torch.Tensor) -> torch.Tensor:
|
|
37
|
+
"""Return the real component of a complex tensor, halving its
|
|
38
|
+
memory footprint.
|
|
39
|
+
|
|
40
|
+
Useful for storing intermediate results when only the magnitude
|
|
41
|
+
(not the phase) is needed, e.g. the accumulated aerial image
|
|
42
|
+
intensity.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
complex_tensor : torch.Tensor
|
|
47
|
+
Complex-valued input tensor.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
torch.Tensor
|
|
52
|
+
Real part as a ``float64`` (or ``float32``) tensor.
|
|
53
|
+
"""
|
|
54
|
+
return complex_tensor.real
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def precision_policy(vram_gb: float = 14) -> Dict[str, torch.dtype]:
|
|
58
|
+
"""Recommend a dtype per operation based on the available VRAM.
|
|
59
|
+
|
|
60
|
+
The policy balances numerical accuracy against memory footprint:
|
|
61
|
+
|
|
62
|
+
- **Low VRAM** (< 6 GiB): use ``complex64`` / ``float32``
|
|
63
|
+
everywhere.
|
|
64
|
+
- **Mid VRAM** (6–12 GiB): use ``complex64`` for intermediate
|
|
65
|
+
buffers, ``float64`` for accumulation.
|
|
66
|
+
- **High VRAM** (≥ 12 GiB): use ``complex128`` / ``float64``
|
|
67
|
+
for full double-precision accuracy.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
vram_gb : float
|
|
72
|
+
Available GPU memory in GiB (default 14).
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
dict
|
|
77
|
+
Keys:
|
|
78
|
+
- ``spectrum`` — dtype for the mask diffraction spectrum.
|
|
79
|
+
- ``pupil`` — dtype for the pupil transmission.
|
|
80
|
+
- ``image`` — dtype for the accumulated aerial image.
|
|
81
|
+
- ``rcwa_eigen`` — dtype for the RCWA eigenvalue solve.
|
|
82
|
+
"""
|
|
83
|
+
if vram_gb < 6:
|
|
84
|
+
return {
|
|
85
|
+
"spectrum": torch.complex64,
|
|
86
|
+
"pupil": torch.complex64,
|
|
87
|
+
"image": torch.float32,
|
|
88
|
+
"rcwa_eigen": torch.complex64,
|
|
89
|
+
}
|
|
90
|
+
if vram_gb < 12:
|
|
91
|
+
return {
|
|
92
|
+
"spectrum": torch.complex64,
|
|
93
|
+
"pupil": torch.complex64,
|
|
94
|
+
"image": torch.float64, # accumulate in double
|
|
95
|
+
"rcwa_eigen": torch.complex64,
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
"spectrum": torch.complex128,
|
|
99
|
+
"pupil": torch.complex128,
|
|
100
|
+
"image": torch.float64,
|
|
101
|
+
"rcwa_eigen": torch.complex128,
|
|
102
|
+
}
|
euv/accel/vram_budget.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Analytical VRAM estimators for RCWA and Abbe imaging.
|
|
2
|
+
|
|
3
|
+
All formulas are based on the dominant tensor allocations for each
|
|
4
|
+
kernel. Use these estimates to pre-check whether a configuration
|
|
5
|
+
will fit on the target GPU before launching the solver.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def estimate_rcma_vram(
|
|
12
|
+
n_orders: int,
|
|
13
|
+
n_layers: int,
|
|
14
|
+
dtype_bytes: int = 16,
|
|
15
|
+
) -> int:
|
|
16
|
+
"""Estimate the GPU memory required for an RCWA solve.
|
|
17
|
+
|
|
18
|
+
Dominant allocation: the per-layer eigenproblem in the Fourier
|
|
19
|
+
modal method. The eigenvalue matrix has shape ``(M, M)`` where
|
|
20
|
+
``M = 2 * n_orders + 1``, and the S-matrix cascade stores a
|
|
21
|
+
handful of ``(M, M)`` matrices per layer.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
n_orders : int
|
|
26
|
+
Number of Fourier orders (positive integer, :math:`M = 2n+1`).
|
|
27
|
+
n_layers : int
|
|
28
|
+
Number of vertical layers in the mask stack.
|
|
29
|
+
dtype_bytes : int
|
|
30
|
+
Bytes per complex element (default 16 for ``complex128``).
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
int
|
|
35
|
+
Estimated memory in bytes.
|
|
36
|
+
"""
|
|
37
|
+
M = 2 * n_orders + 1
|
|
38
|
+
# Per layer: eigenvalue matrix A (M×M), eigenvectors W (M×M),
|
|
39
|
+
# eigenvalues q² (M,), S-matrix cascade (a few M×M).
|
|
40
|
+
# Rough rule: 4 × M² per layer.
|
|
41
|
+
bytes_per_layer = 4 * M * M * dtype_bytes
|
|
42
|
+
total = n_layers * bytes_per_layer
|
|
43
|
+
|
|
44
|
+
# Additional overhead for the profile and Toeplitz matrices
|
|
45
|
+
overhead = 128 * 1024 * 1024 # 128 MB safety margin
|
|
46
|
+
return total + overhead
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def estimate_abbe_vram(
|
|
50
|
+
grid_size: int,
|
|
51
|
+
n_source_points: int,
|
|
52
|
+
dtype_bytes: int = 8,
|
|
53
|
+
) -> int:
|
|
54
|
+
"""Estimate the GPU memory for a full Abbe aerial-image solve.
|
|
55
|
+
|
|
56
|
+
The dominant memory comes from the intermediate coherent images
|
|
57
|
+
accumulated over source points (each image is ``grid_size ×
|
|
58
|
+
grid_size × dtype_bytes``).
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
grid_size : int
|
|
63
|
+
Pixel grid dimension (``G × G``).
|
|
64
|
+
n_source_points : int
|
|
65
|
+
Number of non-zero illumination source points.
|
|
66
|
+
dtype_bytes : int
|
|
67
|
+
Bytes per element of the accumulated image (default 8 for
|
|
68
|
+
``float64``).
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
int
|
|
73
|
+
Estimated memory in bytes.
|
|
74
|
+
"""
|
|
75
|
+
# Per source point: shifted spectrum (G×G complex128), filtered
|
|
76
|
+
# (G×G complex128), IFFT (G×G complex128), intensity (G×G float64).
|
|
77
|
+
# The accumulation buffer is G×G float64.
|
|
78
|
+
# Roughly 4 × G² × 16 + G² × 8 bytes per iteration,
|
|
79
|
+
# but we estimate total peak as the sum of one iteration's working
|
|
80
|
+
# set plus the accumulation buffer.
|
|
81
|
+
gb_per_source = 4 * grid_size * grid_size * 16 # 4 complex buffers
|
|
82
|
+
acc_buf = grid_size * grid_size * 8 # float64 accumulator
|
|
83
|
+
peak = gb_per_source + acc_buf
|
|
84
|
+
# Overhead: pupil, source, coordinate grids
|
|
85
|
+
overhead = 64 * 1024 * 1024 # 64 MB
|
|
86
|
+
return peak + overhead
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def max_harmonics_for_vram(
|
|
90
|
+
vram_budget_gb: float = 14,
|
|
91
|
+
n_layers: int = 12,
|
|
92
|
+
) -> int:
|
|
93
|
+
"""Return the maximum number of Fourier orders (:math:`M`) that fit
|
|
94
|
+
in *vram_budget_gb*.
|
|
95
|
+
|
|
96
|
+
Uses the RCWA estimate (dominant memory scales as
|
|
97
|
+
:math:`M^2`). Walks upward from ``M = 3`` until the budget is
|
|
98
|
+
exceeded.
|
|
99
|
+
|
|
100
|
+
Parameters
|
|
101
|
+
----------
|
|
102
|
+
vram_budget_gb : float
|
|
103
|
+
Available GPU memory in GiB (default 14 — typical consumer
|
|
104
|
+
card with 16 GB physical and ~2 GB reserved by the OS).
|
|
105
|
+
n_layers : int
|
|
106
|
+
Number of RCWA layers (default 12).
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
int
|
|
111
|
+
Maximum odd *M* (number of Fourier orders) that fits.
|
|
112
|
+
"""
|
|
113
|
+
budget_bytes = int(vram_budget_gb * (1024**3))
|
|
114
|
+
|
|
115
|
+
# RCWA orders: M = 2 * n_orders + 1, so n_orders = (M-1)//2
|
|
116
|
+
# We search for the largest odd M that fits.
|
|
117
|
+
lo, hi = 3, 3
|
|
118
|
+
# Exponential probe
|
|
119
|
+
while estimate_rcma_vram((hi - 1) // 2, n_layers) <= budget_bytes:
|
|
120
|
+
hi *= 2
|
|
121
|
+
if hi > 2000: # safety cap
|
|
122
|
+
break
|
|
123
|
+
|
|
124
|
+
best = 3
|
|
125
|
+
while lo <= hi:
|
|
126
|
+
mid = (lo + hi) // 2
|
|
127
|
+
M = mid if mid % 2 == 1 else mid + 1 # ensure odd
|
|
128
|
+
n_orders = (M - 1) // 2
|
|
129
|
+
if estimate_rcma_vram(n_orders, n_layers) <= budget_bytes:
|
|
130
|
+
best = M
|
|
131
|
+
lo = M + 2
|
|
132
|
+
else:
|
|
133
|
+
hi = M - 2
|
|
134
|
+
return best
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def check_oom(total_bytes: int, vram_gb: float = 14) -> None:
|
|
138
|
+
"""Raise :class:`MemoryError` if *total_bytes* exceeds the VRAM
|
|
139
|
+
budget.
|
|
140
|
+
|
|
141
|
+
Parameters
|
|
142
|
+
----------
|
|
143
|
+
total_bytes : int
|
|
144
|
+
Estimated memory requirement in bytes.
|
|
145
|
+
vram_gb : float
|
|
146
|
+
Available VRAM in GiB (default 14).
|
|
147
|
+
|
|
148
|
+
Raises
|
|
149
|
+
------
|
|
150
|
+
MemoryError
|
|
151
|
+
If the estimated memory exceeds the budget.
|
|
152
|
+
"""
|
|
153
|
+
budget = vram_gb * (1024**3)
|
|
154
|
+
if total_bytes > budget:
|
|
155
|
+
used_gb = total_bytes / (1024**3)
|
|
156
|
+
raise MemoryError(
|
|
157
|
+
f"Estimated memory {used_gb:.2f} GiB exceeds "
|
|
158
|
+
f"budget of {vram_gb:.1f} GiB. Reduce problem size "
|
|
159
|
+
"(e.g. fewer Fourier orders, fewer layers, or smaller "
|
|
160
|
+
"grid) or switch to chunked processing."
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def vram_report() -> str:
|
|
165
|
+
"""Return a formatted string with VRAM estimates for common
|
|
166
|
+
OpEnUV configurations.
|
|
167
|
+
|
|
168
|
+
The report tabulates the RCWA and Abbe memory footprints
|
|
169
|
+
across a range of typical problem sizes so the user can
|
|
170
|
+
quickly gauge hardware requirements.
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
str
|
|
175
|
+
"""
|
|
176
|
+
lines = [
|
|
177
|
+
"=" * 72,
|
|
178
|
+
" OpEnUV — VRAM Budget Report",
|
|
179
|
+
"=" * 72,
|
|
180
|
+
"",
|
|
181
|
+
" RCWA estimates (complex128, 12 layers):",
|
|
182
|
+
" " + "-" * 50,
|
|
183
|
+
f" {'Orders':>8} {'M':>5} {'Est. VRAM':>12}",
|
|
184
|
+
" " + "-" * 30,
|
|
185
|
+
]
|
|
186
|
+
for n_orders in [11, 15, 21, 31, 41, 51]:
|
|
187
|
+
M = 2 * n_orders + 1
|
|
188
|
+
est = estimate_rcma_vram(n_orders, n_layers=12)
|
|
189
|
+
est_gb = est / (1024**3)
|
|
190
|
+
lines.append(f" {n_orders:>8} {M:>5} {est_gb:>10.2f} GiB")
|
|
191
|
+
|
|
192
|
+
lines += [
|
|
193
|
+
"",
|
|
194
|
+
" Abbe estimates (complex128, float64 accumulator):",
|
|
195
|
+
" " + "-" * 50,
|
|
196
|
+
f" {'Grid':>8} {'Src pts':>8} {'Est. VRAM':>12}",
|
|
197
|
+
" " + "-" * 30,
|
|
198
|
+
]
|
|
199
|
+
for grid in [128, 256, 512, 1024]:
|
|
200
|
+
for n_src in [100, 500]:
|
|
201
|
+
est = estimate_abbe_vram(grid, n_src)
|
|
202
|
+
est_gb = est / (1024**3)
|
|
203
|
+
lines.append(f" {grid:>4}² {n_src:>8} {est_gb:>10.2f} GiB")
|
|
204
|
+
|
|
205
|
+
lines += [
|
|
206
|
+
"",
|
|
207
|
+
" Max Fourier orders (M) for 14 GiB budget:",
|
|
208
|
+
f" M_max = {max_harmonics_for_vram(14)}",
|
|
209
|
+
"=" * 72,
|
|
210
|
+
]
|
|
211
|
+
return "\n".join(lines)
|
euv/aerial/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Aerial image formation — partially coherent imaging.
|
|
2
|
+
|
|
3
|
+
Submodules
|
|
4
|
+
----------
|
|
5
|
+
abbe
|
|
6
|
+
Abbe's method: sum over illumination source points for the
|
|
7
|
+
physically correct aerial image with mask-3D corrections.
|
|
8
|
+
hopkins
|
|
9
|
+
Hopkins/TCC method: precomputed transmission cross coefficient
|
|
10
|
+
for accelerated O(N² log N) imaging via sum of coherent systems
|
|
11
|
+
(SOCS). Thin-mask approximation suitable for OPC loops.
|
|
12
|
+
pupil
|
|
13
|
+
Projection pupil: numerical aperture, anamorphic (4×/8× for
|
|
14
|
+
High-NA EUV), Zernike aberrations, flare.
|
|
15
|
+
source
|
|
16
|
+
Illumination source shapes: conventional (disk), annular,
|
|
17
|
+
dipole (XY), quasar (CQuad), and custom free-form.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|