Workflow Engine¶
openlithohub.workflow.parsing
¶
Layout file parsing via KLayout Python API.
parse_layout(path, *, lef_files=None)
¶
Parse an OASIS / GDSII / DEF / LEF layout file.
Returns dictionary with 'cells', 'layers', 'bounding_box', and '_layout' handle.
DEF (Design Exchange Format, IEEE 1481) is the standard placed-and-routed
layout dump produced by Innovus / ICC2 / OpenROAD. DEF carries placement
+ routing geometry but not cell internals — those live in companion
LEF files. Pass lef_files=[...] to feed cell abstracts so DEF
components resolve to real polygons rather than empty placeholders.
LEF-only inputs (.lef) are also accepted, for tools that want to
inspect cell abstracts without a placed design.
Source code in src/openlithohub/workflow/parsing.py
openlithohub.workflow.tiling
¶
Full-chip tiling strategy for distributed processing.
Tile
dataclass
¶
tile_layout(layout_tensor, tile_size=2048, overlap=128)
¶
Partition a full-chip layout tensor into overlapping tiles.
Uses a sliding window with configurable overlap. Boundary tiles are
anchored to the layout edge (origin pulled back so the tile fits
entirely inside the layout) so the model sees real layout context
instead of zero-padding artefacts. The resulting boundary tiles overlap
further with their neighbours, which stitch_tiles blends correctly
via its weight-map normalization.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
layout_tensor
|
Tensor
|
Full layout as tensor (H, W). |
required |
tile_size
|
int
|
Size of each square tile in pixels. |
2048
|
overlap
|
int
|
Overlap between adjacent tiles for seamless stitching. |
128
|
Returns:
| Type | Description |
|---|---|
list[Tile]
|
List of Tile objects covering the full layout. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If overlap >= tile_size or tile_size <= 0. |
Source code in src/openlithohub/workflow/tiling.py
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | |
stitch_tiles(tiles, output_shape)
¶
Reassemble optimized tiles into a full-chip tensor.
Uses linear blending in overlap regions to avoid seam artifacts.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tiles
|
list[tuple[Tile, Tensor]]
|
List of (original_tile, optimized_tensor) pairs. |
required |
output_shape
|
tuple[int, int]
|
(H, W) of the full output tensor. |
required |
Returns:
| Type | Description |
|---|---|
Tensor
|
Stitched tensor of shape output_shape. |
Source code in src/openlithohub/workflow/tiling.py
openlithohub.workflow.halo
¶
Process-node-aware tile halo sizing (RFC 0005).
The halo is the per-tile guard band of overlapping pixels that lets the forward lithography model see real layout context at tile boundaries instead of zero-padded artefacts. Two physical phenomena set the lower bound:
- Optical interaction radius (OIR) — light at a tile boundary sees
neighbours through the imaging kernel; carried by
ProcessNodeConfig.optical_radius_nm. - Model receptive field — convolutional models also propagate
information across pixels; carried by
LithographyModel.RECEPTIVE_FIELD_PX.
This module centralises the math that picks max(OIR_px, RF_px),
rounds up to a stride-friendly multiple, and clamps to fit inside the
caller's tile size.
DEFAULT_HALO_PX = 128
module-attribute
¶
Pre-RFC-0005 fixed halo. Used when neither node nor model is provided.
compute_halo_px(node, model, pixel_nm, tile_size)
¶
Pick a tile halo big enough for the optical kernel and the model RF.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node
|
ProcessNodeConfig | None
|
Process node carrying |
required |
model
|
LithographyModel | None
|
Model carrying |
required |
pixel_nm
|
float
|
Pixel pitch in nanometers (used to convert OIR → pixels). |
required |
tile_size
|
int
|
Tile width in pixels. The returned halo is clamped so
that |
required |
Returns:
| Type | Description |
|---|---|
int
|
Halo size in pixels. |
int
|
|
Source code in src/openlithohub/workflow/halo.py
describe_halo(halo_px, node, model, pixel_nm)
¶
One-line provenance string for the resolved halo, for CLI logging.
Source code in src/openlithohub/workflow/halo.py
openlithohub.workflow.parallel
¶
Multi-process tile inference for openlithohub optimize run.
RFC 0004 picks torch.multiprocessing.spawn over tile shards as the v0.3
direction. The model layer stays untouched so ONNX/TorchScript export
keeps returning a bare nn.Module; parallelism wraps the tile loop,
not the model.
v0.7 (WS-E): shared-weight dispatch. The parent process loads model weights into CPU shared memory before spawning workers. Workers memory-map the weights instead of independently re-instantiating and reloading, reducing peak memory from O(N × model_size) to O(model_size + N × activation_size).
Compile-cache: when --compile is active, the Inductor cache directory
is set in the parent so workers reuse compiled kernels without per-worker
recompilation.
parallel_tile_inference(model_name, model_kwargs, tiles, *, num_gpus, base_perf_kwargs, progress_cb=None)
¶
Shard tiles round-robin across num_gpus worker processes.
The parent loads model weights into CPU shared memory before spawning. Workers memory-map these weights, reducing peak memory from O(N x model_size) to approximately O(model_size + N x activation_size).
Falls back to CPU dispatch when fewer than num_gpus CUDA devices
are visible — this is what makes CPU-only CI exercise the dispatch
logic.
Source code in src/openlithohub/workflow/parallel.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | |
openlithohub.workflow.contour.manhattan
¶
Manhattan (staircase) contour extraction for traditional VSB writers.
extract_manhattan_contour(mask, pixel_size_nm=1.0)
¶
Extract Manhattan (rectilinear) polygon contours from a binary mask.
Traces pixel-edge boundaries between foreground and background regions, producing axis-aligned polygons suitable for VSB mask writers.
The algorithm: 1. Find horizontal and vertical boundary edges between 0/1 pixels 2. Build an adjacency graph of edges sharing vertices 3. Trace closed loops through the edge graph
Vertices are at pixel corners (not pixel centers), scaled by pixel_size_nm.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mask
|
Tensor
|
Binary mask tensor (H, W). |
required |
pixel_size_nm
|
float
|
Physical pixel size for coordinate scaling. |
1.0
|
Returns:
| Type | Description |
|---|---|
list[list[tuple[float, float]]]
|
List of polygons, each as a list of (x_nm, y_nm) vertices in order. |
list[list[tuple[float, float]]]
|
Outer boundaries are clockwise, holes are counter-clockwise. |
Source code in src/openlithohub/workflow/contour/manhattan.py
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | |
openlithohub.workflow.contour.curvilinear
¶
Curvilinear contour extraction and B-spline fitting for OASIS export.
Curvilinear masks (post-ILT, EUV, MBMW writers) are emitted as high-resolution
sampled polygons on a designated layer of a real OASIS file. Native SEMI P44
curve primitives are not yet emitted — klayout.db does not surface them in
its public Python API at the time of writing — but the file produced here is
a valid OASIS file that any vendor tool can read.
BSplineCurve
dataclass
¶
fit_bspline(contour_pixels, tolerance_nm=0.5, pixel_size_nm=1.0, *, warn_on_skip=True)
¶
Fit B-spline curves to pixel-level contour data.
If input is a 2D binary mask, extracts boundary contours first. If input is an (N, 2) tensor, treats it as a single ordered point loop.
Loops with fewer than 5 points (the minimum for a cubic periodic
spline) and loops where splprep fails to converge are skipped.
Both used to be silently dropped — small features (SRAFs, sharp
points) would disappear from the OASIS export with no signal. Now a
UserWarning is emitted per skipped loop unless
warn_on_skip=False; callers that intentionally feed mixed-size
geometry can opt out.
The smoothing factor passed to splprep is tolerance_nm**2 *
n_points (matches scipy's "sum of squared residuals" semantics) so
long contours are not over-smoothed relative to short ones.
Source code in src/openlithohub/workflow/contour/curvilinear.py
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | |
export_oasis_mbw(curves, output_path, *, samples_per_curve=64, pixel_size_nm=1.0, layer=1, datatype=0, cell_name='TOP', min_area_nm2=0.0, vertex_tolerance_nm=0.0)
¶
Serialize B-spline curves to an OASIS file via klayout.db.
Curves are sampled to high-resolution polygons (samples_per_curve
vertices per loop) and inserted into a single top cell on the requested
(layer, datatype). The output is a real SEMI P39 OASIS file readable
by KLayout, Calibre, and other industry tools — not a custom binary
blob.
Native SEMI P44 multi-beam curve primitives are not yet emitted; the polygon approximation is the standard interim representation that all multi-beam writer flows accept.
min_area_nm2 is an opt-in filter for sub-resolution islands: any
sampled polygon with absolute area below this threshold is dropped
before insertion. Default 0.0 keeps every shape so academic /
Hackathon evaluation stays bit-exact. A positive value is intended for
fab-ready exports where MRC would otherwise reject the smallest SRAFs
a curvilinear ILT can produce; the count of dropped shapes is logged
at INFO level so the filter is auditable.
vertex_tolerance_nm is an opt-in Ramer-Douglas-Peucker simplification
on each sampled polygon: any vertex within this perpendicular distance
of the chord between its surviving neighbours is dropped. Default 0.0
keeps every sampled vertex (bit-exact academic behaviour). Positive
values cut OASIS file size dramatically on smooth ILT contours — a
multi-beam mask writer (MBMW) consumes shots and bytes, so 0.5 nm
typically halves vertex count without measurable wafer-image change.
Source code in src/openlithohub/workflow/contour/curvilinear.py
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 | |
openlithohub.workflow.export
¶
OASIS/GDSII export coordination.
export_oasis(mask, output_path, *, mode='curvilinear', pixel_size_nm=1.0, min_area_nm2=0.0)
¶
Export an optimized mask tensor to OASIS format.
For manhattan mode, extracts rectilinear contours and writes via KLayout.
For curvilinear mode, fits B-splines and writes a curvilinear OASIS file
(sampled polygons on a designated layer; see contour.curvilinear).
Native SEMI P39 (OASIS.MASK) curve primitives and SEMI P44 multi-beam
mask-writer input are tracked separately and not yet emitted here.
min_area_nm2 (curvilinear only) drops sub-resolution islands below
the given polygon area before writing. Default 0.0 keeps every
shape so academic / Hackathon evaluation stays bit-exact.
Source code in src/openlithohub/workflow/export.py
export_gds(mask, output_path, *, mode='curvilinear', pixel_size_nm=1.0, samples_per_curve=64, min_area_nm2=0.0)
¶
Export an optimized mask tensor to GDSII format.
GDSII is the academic / contest lingua franca (ICCAD, SPIE benchmarks
and the cuLitho whitepaper all use .gds); OASIS is dominant in
mask-shop flows. This function covers the academic path so users do
not have to convert .oas → .gds themselves before running our
benchmark on contest-style inputs.
Same routing as :func:export_oasis: mode="manhattan" extracts
rectilinear polygons; mode="curvilinear" fits B-splines, samples
them to polygons (GDSII has no native curve primitive) and writes
a polygon-only .gds via KLayout. The polygon density is controlled
by samples_per_curve and matches the OASIS curvilinear writer's
default — a curvilinear OASIS and a curvilinear GDS exported from the
same mask are visually identical, but the GDS file is larger because
every curve becomes an explicit vertex list.
Source code in src/openlithohub/workflow/export.py
openlithohub.workflow.process_node
¶
DTCO process node configuration for lithography simulation parameters.
ProcessNodeConfig
dataclass
¶
Physical parameters for a specific semiconductor process node.
These parameters configure the forward lithography model, compliance checks, and optimization targets based on the target manufacturing technology.
Source code in src/openlithohub/workflow/process_node.py
optical_radius_nm = 1500.0
class-attribute
instance-attribute
¶
Optical interaction radius — how far light at a tile boundary
'sees' through the imaging kernel. Used by workflow.halo to size
tile halos so the forward model is fed real layout context, not
zero-padded boundaries. Conservative default of 1.5 µm matches DUV
rule-of-thumb (~10 × λ / (2 × NA)); EUV nodes can run much tighter.
demag_slit = 4.0
class-attribute
instance-attribute
¶
Reticle-to-wafer demagnification ratios along the scan and slit axes.
Standard low-NA EUV (NA=0.33) and DUV scanners are isotropic 4×, so
both default to 4.0. High-NA EUV (NA=0.55, ASML EXE:5000 class) is
anamorphic: 8× along the scan axis and 4× along the slit axis,
halving the field on-wafer to ~26 × 16.5 mm. The Hopkins forward
currently assumes an isotropic mask grid and ignores these flags;
they are recorded here so downstream tooling (anamorphic imaging,
half-field stitching, mask-side pixel sizing) can branch on
demag_scan != demag_slit. See the High-NA tracking issue.
multi_patterning = 'none'
class-attribute
instance-attribute
¶
Multi-patterning scheme assumed by min_feature_nm / min_spacing_nm.
The Rayleigh limit k1 × λ / NA caps the half-pitch a single
exposure can resolve at k1 ≈ 0.25 (production manufacturable). When
min_feature_nm falls below that limit, the layout assumes
multiple exposures (LELE / LELELE / SADP / SAQP) — a single-shot
forward simulation is then a coarse approximation, valid for
one of the multi-patterning sub-layers but not the composite
image.
Values: "none", "lele" (litho-etch-litho-etch, 2-colour
decomposition), "lelele" (3-colour), "sadp" (self-aligned
double patterning), "saqp" (self-aligned quadruple). The flag
is informational — the forward model does not yet decompose layouts
by colour, so callers running below the single-exposure k1=0.25
floor should split layouts manually before scoring.
Worked examples
- 28nm at 193 nm DUV / NA 1.35: single-exposure k1 = 14 × 1.35 /
193 ≈ 0.098, well below 0.25. Production 28nm is LELE
immersion. Marked
"lele". - 7nm at EUV NA 0.33: k1 = 14 × 0.33 / 13.5 ≈ 0.34. Single
exposure, marked
"none".
is_anamorphic
property
¶
Whether the projection optics are anamorphic (High-NA EUV).
sigma_px
property
¶
Compute Gaussian PSF sigma in pixels from optical parameters.
k1_factor
property
¶
Rayleigh k1 factor for minimum half-pitch.
get_node(name)
¶
Get a process node configuration by name.
Raises:
| Type | Description |
|---|---|
KeyError
|
If the node name is not found in presets. |
Source code in src/openlithohub/workflow/process_node.py
openlithohub.workflow.process_window
¶
Process-window-aware OPC ("PW-OPC") workflow.
Production OPC must hold up across a dose/focus process window, not just at nominal exposure. Optimising against the nominal corner alone tends to produce masks that look great in the lab and fail in the fab.
This module supplies the corner-sweep machinery: define a small set of dose +
defocus corners, run the existing forward model at each, and aggregate the
fidelity loss as a weighted mean. It is a drop-in replacement for the nominal
F.mse_loss(resist, target) line that today lives inside every ILT inner
loop — see models.levelset_ilt.LevelSetILTModel for the canonical
integration site.
The corner enumeration mirrors the four-corner scheme used by
benchmark.metrics.pvband.compute_pvband so that what we optimise and what
we measure live in the same world.
Physical caveats (issue #27)¶
The corner sweep here is a fast inner-loop diagnostic, not rigorous PW simulation:
- Defocus is modelled by widening the Gaussian PSF only. A real
defocused pupil loses contrast (the MTF dips and re-rings — the
textbook Bossung curves) — the energy is redistributed into the
pupil's nulls, not merely smeared by a wider real-space kernel. A
Gaussian preserves total energy, so this proxy under-estimates
defocus-induced contrast loss; PW corners that are dim in reality
look only blurry here. For headline PW numbers, drive the Hopkins
path with measured-source / Zernike-pupil I/O (see
optics.py) rather than this proxy. - Dose enters as a multiplicative scale on the aerial image. That
is correct if the resist threshold is dose-pinned (the intensity
the resist clears at scales linearly with dose). The
HopkinsSimulatordoes this internally (issue #52), but this fast path uses a fixed threshold passed by the caller — so a ±5% dose corner cleanly shifts the resist contour rather than being silently cancelled. Pair it withthreshold=0.225(LithoBench-canonical) for headline alignment with the Hopkins benchmark numbers.
If you require physically-rigorous defocus, use a Hopkins SOCS forward sim with a defocus Zernike (Z4) and treat this module as the training-loop proxy.
ProcessWindowCorner
dataclass
¶
One dose/focus corner in the optimisation sweep.
sigma_px is the Gaussian-PSF width in pixels — defocus translates into
sigma upstream of this dataclass (callers convert nm → px), keeping this
structure agnostic to physical units.
Source code in src/openlithohub/workflow/process_window.py
pw_aerial_images(mask, corners=DEFAULT_PW_CORNERS)
¶
Simulate the aerial image at every corner. :stable:
Returned tensors share rank with mask ((H,W) in, (H,W) out).
Autograd-connected — gradients flow back to mask.
Source code in src/openlithohub/workflow/process_window.py
pw_fidelity_loss(mask, target, *, corners=DEFAULT_PW_CORNERS, threshold=0.5, steepness=50.0, resist_diffusion_nm=0.0, pixel_size_nm=1.0, quencher=0.0)
¶
Weighted-mean MSE between simulated resist and target across PW corners. :stable:
Each corner contributes weight * MSE(resist_corner, target); the result
is divided by the sum of weights so the scalar magnitude stays comparable
to the nominal-only baseline.
With corners=(ProcessWindowCorner(dose=1.0, sigma_px=σ, weight=1.0),)
this reduces to the existing nominal-only loss, which is how the call-site
in LevelSetILTModel keeps backward compatibility.
.. note::
Threshold default is 0.5 (legacy API — changing it would silently
shift every training trajectory built on this module). Pass
threshold=0.225 to align with the LithoBench / Yang2023
resist-clearing convention used by compute_l2_error /
compute_wafer_epe.
Source code in src/openlithohub/workflow/process_window.py
openlithohub.workflow.eda_bridge
¶
Bridge scripts for commercial EDA tools (Calibre, IC Validator).
OpenLithoHub does not implement full DRC — these helpers emit minimal,
human-editable rule decks that load an exported .oas and run the most
basic checks (min width, min spacing). The intent is to remove the friction
of "I have OASIS, now what?" for layout engineers, not to replace a real
sign-off deck.
The emitted files are pure text. No EDA tool is invoked here.
Unit handling (issue #50)¶
The Calibre template uses PRECISION 1000 (1000 dbu/µm = 1 dbu/nm)
and threshold literals are emitted in microns (min_width_nm /
1000). SVRF interprets bare numerics on INTERNAL ... < N /
EXTERNAL ... < N directives as user units (microns by default), so
emitting < 40 for a 40-nm rule would have meant "< 40 µm" — i.e.
the rule check would be 1000× too lax and would never flag a real
violation. The IC Validator template has the same convention.
Run any deck-emission test that pins literal text against this nm-vs-µm shift if you change the format strings.
Geometric scope (issue #51)¶
The Calibre INTERNAL mask < N ABUT < 90 SINGULAR REGION and the ICV
internal1(mask, < N) directives are measured edge-to-edge, which
is well-defined for Manhattan polygons but can produce direction-dependent
results on curvilinear / concave geometry (the classic case: a notch on
the inside of an arc, where the closest "internal" pair of edges is the
arc-tangent rather than the notch wall). For Manhattan OPC output (the
default of workflow.contour.manhattan) this template is correct. For
curvilinear ILT output (workflow.contour.curvilinear), replace
INTERNAL ... ABUT < 90 with INTERNAL ... PROJECTING < 90 (Calibre)
or pair internal1 with a with_radius curvature filter (ICV) so
arc-segment curvature is not mis-classified as a width violation. We
ship the Manhattan-correct deck because it's the headline OPC path; the
curvilinear deck is a roadmap item.
BridgeRules
dataclass
¶
emit_calibre_svrf(oasis_path, rules, *, cell_name='TOP', output_path=None)
¶
Emit a minimal Calibre nmDRC .svrf rule deck next to the OASIS file.
Source code in src/openlithohub/workflow/eda_bridge.py
emit_icv_runset(oasis_path, rules, *, cell_name='TOP', output_path=None)
¶
Emit a minimal Synopsys IC Validator runset next to the OASIS file.
Source code in src/openlithohub/workflow/eda_bridge.py
emit_bridge_bundle(oasis_path, rules, *, cell_name='TOP')
¶
Emit Calibre + IC Validator templates and a short README for the bundle.