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.
Files changed (150) hide show
  1. euv/__init__.py +19 -0
  2. euv/accel/__init__.py +25 -0
  3. euv/accel/chunked.py +124 -0
  4. euv/accel/device.py +88 -0
  5. euv/accel/mixed_precision.py +102 -0
  6. euv/accel/vram_budget.py +211 -0
  7. euv/aerial/__init__.py +20 -0
  8. euv/aerial/abbe.py +343 -0
  9. euv/aerial/hopkins.py +330 -0
  10. euv/aerial/pupil.py +206 -0
  11. euv/aerial/source.py +193 -0
  12. euv/api/__init__.py +11 -0
  13. euv/api/main.py +257 -0
  14. euv/api/schemas.py +171 -0
  15. euv/api/static/app.js +289 -0
  16. euv/api/static/index.html +164 -0
  17. euv/api/static/simulate.html +688 -0
  18. euv/api/static/style.css +533 -0
  19. euv/calibrate/__init__.py +58 -0
  20. euv/calibrate/wafer_fit.py +455 -0
  21. euv/constants.py +108 -0
  22. euv/data/cxro/Ac.csv +517 -0
  23. euv/data/cxro/Ag.csv +509 -0
  24. euv/data/cxro/Al.csv +505 -0
  25. euv/data/cxro/Ar.csv +507 -0
  26. euv/data/cxro/As.csv +507 -0
  27. euv/data/cxro/At.csv +517 -0
  28. euv/data/cxro/Au.csv +507 -0
  29. euv/data/cxro/B.csv +503 -0
  30. euv/data/cxro/Ba.csv +509 -0
  31. euv/data/cxro/Be.csv +725 -0
  32. euv/data/cxro/Bi.csv +517 -0
  33. euv/data/cxro/Br.csv +507 -0
  34. euv/data/cxro/C.csv +503 -0
  35. euv/data/cxro/Ca.csv +505 -0
  36. euv/data/cxro/Cd.csv +511 -0
  37. euv/data/cxro/Ce.csv +509 -0
  38. euv/data/cxro/Cl.csv +505 -0
  39. euv/data/cxro/Co.csv +505 -0
  40. euv/data/cxro/Cr.csv +752 -0
  41. euv/data/cxro/Cs.csv +509 -0
  42. euv/data/cxro/Cu.csv +505 -0
  43. euv/data/cxro/Dy.csv +515 -0
  44. euv/data/cxro/Er.csv +515 -0
  45. euv/data/cxro/Eu.csv +515 -0
  46. euv/data/cxro/F.csv +503 -0
  47. euv/data/cxro/Fe.csv +505 -0
  48. euv/data/cxro/Fr.csv +517 -0
  49. euv/data/cxro/Ga.csv +507 -0
  50. euv/data/cxro/Gd.csv +720 -0
  51. euv/data/cxro/Ge.csv +507 -0
  52. euv/data/cxro/H.csv +502 -0
  53. euv/data/cxro/He.csv +502 -0
  54. euv/data/cxro/Hf.csv +515 -0
  55. euv/data/cxro/Hg.csv +517 -0
  56. euv/data/cxro/Ho.csv +515 -0
  57. euv/data/cxro/I.csv +509 -0
  58. euv/data/cxro/In.csv +511 -0
  59. euv/data/cxro/Ir.csv +516 -0
  60. euv/data/cxro/K.csv +504 -0
  61. euv/data/cxro/Kr.csv +510 -0
  62. euv/data/cxro/La.csv +712 -0
  63. euv/data/cxro/Li.csv +502 -0
  64. euv/data/cxro/Lu.csv +515 -0
  65. euv/data/cxro/Mg.csv +756 -0
  66. euv/data/cxro/Mn.csv +505 -0
  67. euv/data/cxro/Mo.csv +910 -0
  68. euv/data/cxro/N.csv +504 -0
  69. euv/data/cxro/Na.csv +505 -0
  70. euv/data/cxro/Nb.csv +758 -0
  71. euv/data/cxro/Nd.csv +509 -0
  72. euv/data/cxro/Ne.csv +504 -0
  73. euv/data/cxro/Ni.csv +505 -0
  74. euv/data/cxro/O.csv +503 -0
  75. euv/data/cxro/Os.csv +517 -0
  76. euv/data/cxro/P.csv +505 -0
  77. euv/data/cxro/Pa.csv +517 -0
  78. euv/data/cxro/Pb.csv +517 -0
  79. euv/data/cxro/Pd.csv +508 -0
  80. euv/data/cxro/Pm.csv +509 -0
  81. euv/data/cxro/Po.csv +517 -0
  82. euv/data/cxro/Pr.csv +509 -0
  83. euv/data/cxro/Pt.csv +946 -0
  84. euv/data/cxro/Ra.csv +517 -0
  85. euv/data/cxro/Rb.csv +509 -0
  86. euv/data/cxro/Re.csv +517 -0
  87. euv/data/cxro/Rh.csv +509 -0
  88. euv/data/cxro/Rn.csv +517 -0
  89. euv/data/cxro/Ru.csv +616 -0
  90. euv/data/cxro/S.csv +505 -0
  91. euv/data/cxro/Sb.csv +509 -0
  92. euv/data/cxro/Sc.csv +599 -0
  93. euv/data/cxro/Se.csv +507 -0
  94. euv/data/cxro/Si.csv +757 -0
  95. euv/data/cxro/Sm.csv +509 -0
  96. euv/data/cxro/Sn.csv +511 -0
  97. euv/data/cxro/Sr.csv +509 -0
  98. euv/data/cxro/Ta.csv +717 -0
  99. euv/data/cxro/Tb.csv +515 -0
  100. euv/data/cxro/Tc.csv +509 -0
  101. euv/data/cxro/Te.csv +509 -0
  102. euv/data/cxro/Th.csv +517 -0
  103. euv/data/cxro/Ti.csv +531 -0
  104. euv/data/cxro/Tl.csv +517 -0
  105. euv/data/cxro/Tm.csv +515 -0
  106. euv/data/cxro/U.csv +515 -0
  107. euv/data/cxro/V.csv +505 -0
  108. euv/data/cxro/W.csv +542 -0
  109. euv/data/cxro/Xe.csv +510 -0
  110. euv/data/cxro/Y.csv +620 -0
  111. euv/data/cxro/Yb.csv +515 -0
  112. euv/data/cxro/Zn.csv +505 -0
  113. euv/data/cxro/Zr.csv +725 -0
  114. euv/etch/__init__.py +61 -0
  115. euv/etch/bias.py +422 -0
  116. euv/io/__init__.py +33 -0
  117. euv/io/cli.py +426 -0
  118. euv/io/gds.py +330 -0
  119. euv/io/rasterize.py +189 -0
  120. euv/mask3d/__init__.py +16 -0
  121. euv/mask3d/geometry.py +208 -0
  122. euv/mask3d/rcwa2d.py +517 -0
  123. euv/mask3d/rcwa_torch.py +442 -0
  124. euv/materials.py +424 -0
  125. euv/metro/__init__.py +74 -0
  126. euv/metro/cd.py +480 -0
  127. euv/metro/process_window.py +312 -0
  128. euv/metro/sem_render.py +279 -0
  129. euv/opc/__init__.py +13 -0
  130. euv/opc/openilt_bridge.py +235 -0
  131. euv/optics/__init__.py +13 -0
  132. euv/optics/collector.py +184 -0
  133. euv/optics/multilayer.py +315 -0
  134. euv/optics/plot.py +306 -0
  135. euv/optics/tmm.py +411 -0
  136. euv/pipeline.py +356 -0
  137. euv/resist/__init__.py +67 -0
  138. euv/resist/develop.py +375 -0
  139. euv/resist/exposure.py +278 -0
  140. euv/resist/peb.py +372 -0
  141. euv/resist/stochastic.py +642 -0
  142. euv/source/__init__.py +27 -0
  143. euv/source/plasma.py +346 -0
  144. euv/source/spectrum.py +210 -0
  145. openuv-1.0.0.dist-info/METADATA +267 -0
  146. openuv-1.0.0.dist-info/RECORD +150 -0
  147. openuv-1.0.0.dist-info/WHEEL +5 -0
  148. openuv-1.0.0.dist-info/entry_points.txt +2 -0
  149. openuv-1.0.0.dist-info/licenses/LICENSE +201 -0
  150. 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
+ }
@@ -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