Skip to content

layout.readers — reading layout files

tempura/layout/readers.py reads gate geometry from layout files and normalizes it into a single typed payload. load_layout_data(...) and load_layout_polygons(...) are the supported entry points; the per-format readers behind them are an implementation detail.

What it does

A reader returns a LayoutData (see layout.models) carrying the polygons grouped by layer plus the file's unit metadata. The format is chosen from the file suffix:

  • GDS / GDSII and OAS are read with gdstk, which reports the file's unit scale and precision directly. Layer names are normalized to L{layer}D{datatype}.
  • DXF is parsed by a small hand-rolled state machine over the file's group-code/value pairs, collecting closed POLYLINE entities per layer and reading the $INSUNITS header to recover a physical scale when present. DXF layers keep their original names.

Unit metadata

Each LayoutData records whether units are known (units_known), the scale to meters (unit_scale_m), and the source format. This metadata is what lets prepare_layout trust the file's physical size in size_mode="layout_size", or fall back to an explicit length when the units are missing or unreliable.

API

readers

Layout file readers returning polygons grouped by layer.

load_layout_data(path)

Load polygons and available unit metadata from GDS/GDSII/OAS/DXF.

Parameters:

Name Type Description Default
path Path

Filesystem path to the layout file.

required

Returns:

Type Description
LayoutData

Normalized layout payload produced by the suffix-specific reader.

Raises:

Type Description
FileNotFoundError

If path does not exist.

ValueError

If the file suffix is not one of the supported layout formats.

Source code in tempura/layout/readers.py
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
def load_layout_data(path: Path) -> LayoutData:
    """Load polygons and available unit metadata from GDS/GDSII/OAS/DXF.

    Args:
        path: Filesystem path to the layout file.

    Returns:
        Normalized layout payload produced by the suffix-specific reader.

    Raises:
        FileNotFoundError: If ``path`` does not exist.
        ValueError: If the file suffix is not one of the supported layout
            formats.
    """
    if not path.exists():
        raise FileNotFoundError(f"Layout file not found: {path}")

    suffix = path.suffix.lower()
    try:
        reader = LAYOUT_READERS[suffix]
    except KeyError as exc:
        raise ValueError(
            f"Unsupported layout suffix {suffix!r}; use {sorted(SUPPORTED_LAYOUT_SUFFIXES)}"
        ) from exc
    # Dispatch is suffix-based so callers get the same LayoutData contract from
    # every supported file format.
    return reader(path)

load_layout_polygons(path)

Load polygons by layer from GDS/GDSII/OAS/DXF.

Parameters:

Name Type Description Default
path Path

Filesystem path to the layout file.

required

Returns:

Type Description
PolygonMap

Mapping from layer name to the polygons read from path.

Source code in tempura/layout/readers.py
294
295
296
297
298
299
300
301
302
303
def load_layout_polygons(path: Path) -> PolygonMap:
    """Load polygons by layer from GDS/GDSII/OAS/DXF.

    Args:
        path: Filesystem path to the layout file.

    Returns:
        Mapping from layer name to the polygons read from ``path``.
    """
    return load_layout_data(path).polygons_by_layer

read_dxf_layout(path)

Read closed DXF POLYLINE entities and any unit metadata.

Parameters:

Name Type Description Default
path Path

Filesystem path to the DXF file.

required

Returns:

Type Description
LayoutData

Normalized layout payload containing polygons grouped by DXF layer

LayoutData

name plus any recognized $INSUNITS scale information.

Source code in tempura/layout/readers.py
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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
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
def read_dxf_layout(path: Path) -> LayoutData:
    """Read closed DXF ``POLYLINE`` entities and any unit metadata.

    Args:
        path: Filesystem path to the DXF file.

    Returns:
        Normalized layout payload containing polygons grouped by DXF layer
        name plus any recognized ``$INSUNITS`` scale information.
    """
    tokens = path.read_text().splitlines()
    polygons_by_layer: PolygonMap = {}

    in_entities = False
    current_layer: str | None = None
    current_vertices: list[tuple[float, float]] = []
    polyline_open = False
    vertex_open = False
    vertex_x: float | None = None
    vertex_y: float | None = None

    def append_polygon() -> None:
        """Flush the current DXF polyline into ``polygons_by_layer`` if valid."""
        nonlocal current_vertices
        if current_layer is None or len(current_vertices) < 3:
            current_vertices = []
            return
        points = current_vertices
        if np.allclose(points[0], points[-1]):
            points = points[:-1]
        polygons_by_layer.setdefault(current_layer, []).append(gdstk.Polygon(points))
        current_vertices = []

    def close_vertex() -> None:
        """Flush the current DXF vertex into the active polyline if complete."""
        nonlocal vertex_x, vertex_y
        if vertex_x is not None and vertex_y is not None:
            current_vertices.append((vertex_x, vertex_y))
        vertex_x = None
        vertex_y = None

    # DXF stores records as alternating group-code/value lines, so we walk the
    # file as a small state machine over the current polyline and vertex.
    idx = 0
    while idx < len(tokens) - 1:
        # The reader supports the classic POLYLINE/VERTEX/SEQEND sequence. It
        # keeps one active polyline and one active vertex while walking
        # group-code/value pairs.
        code = tokens[idx].strip()
        value = tokens[idx + 1].strip()
        idx += 2

        if code == "0" and value == "SECTION":
            if idx < len(tokens) - 1 and tokens[idx].strip() == "2":
                in_entities = tokens[idx + 1].strip() == "ENTITIES"
            continue
        if not in_entities:
            continue
        if code == "0" and value == "ENDSEC":
            close_vertex()
            append_polygon()
            break
        if code == "0" and value == "POLYLINE":
            # Starting a new polyline closes any incomplete previous entity
            # before resetting the state for the next layer/vertex stream.
            close_vertex()
            append_polygon()
            polyline_open = True
            vertex_open = False
            current_layer = None
            continue
        if not polyline_open:
            continue
        if code == "0" and value == "VERTEX":
            # A VERTEX record starts a new point. The previous point is flushed
            # only after both x and y have been read.
            close_vertex()
            vertex_open = True
            continue
        if code == "0" and value == "SEQEND":
            # SEQEND terminates the active polyline and emits one gdstk polygon
            # if enough vertices were collected.
            close_vertex()
            append_polygon()
            polyline_open = False
            vertex_open = False
            current_layer = None
            continue
        if code == "8":
            current_layer = value
            continue
        if not vertex_open:
            continue
        if code == "10":
            vertex_x = float(value)
        elif code == "20":
            vertex_y = float(value)

    insunits = _read_dxf_insunits(path)
    unit_scale_m = _DXF_INSUNITS_TO_METERS.get(insunits)
    return _layout_data(
        polygons_by_layer,
        unit_scale_m=unit_scale_m,
        unit_source="dxf_header.$INSUNITS" if unit_scale_m is not None else None,
        precision_m=None,
        units_known=unit_scale_m is not None,
        source_format="dxf",
    )

read_dxf_polylines(path)

Read closed DXF POLYLINE entities grouped by DXF layer name.

Parameters:

Name Type Description Default
path Path

Filesystem path to the DXF file.

required

Returns:

Type Description
PolygonMap

Mapping from DXF layer name to closed polyline polygons.

Source code in tempura/layout/readers.py
330
331
332
333
334
335
336
337
338
339
def read_dxf_polylines(path: Path) -> PolygonMap:
    """Read closed DXF ``POLYLINE`` entities grouped by DXF layer name.

    Args:
        path: Filesystem path to the DXF file.

    Returns:
        Mapping from DXF layer name to closed polyline polygons.
    """
    return read_dxf_layout(path).polygons_by_layer

read_gds_layout(path)

Read polygons and unit metadata from a GDS/GDSII layout.

Parameters:

Name Type Description Default
path Path

Filesystem path to the GDS or GDSII file.

required

Returns:

Type Description
LayoutData

Normalized layout payload containing polygons grouped by layer plus the

LayoutData

unit metadata reported by the GDS library.

Source code in tempura/layout/readers.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
def read_gds_layout(path: Path) -> LayoutData:
    """Read polygons and unit metadata from a GDS/GDSII layout.

    Args:
        path: Filesystem path to the GDS or GDSII file.

    Returns:
        Normalized layout payload containing polygons grouped by layer plus the
        unit metadata reported by the GDS library.
    """
    library = gdstk.read_gds(path)
    return _layout_data(
        _group_cell_polygons(library.top_level()),
        unit_scale_m=float(library.unit),
        unit_source="gds_library.unit",
        precision_m=float(library.precision),
        units_known=True,
        source_format="gds",
    )

read_gds_polygons(path)

Read polygons from a GDS/GDSII layout.

Parameters:

Name Type Description Default
path Path

Filesystem path to the GDS or GDSII file.

required

Returns:

Type Description
PolygonMap

Mapping from layer name to polygons read from path.

Source code in tempura/layout/readers.py
306
307
308
309
310
311
312
313
314
315
def read_gds_polygons(path: Path) -> PolygonMap:
    """Read polygons from a GDS/GDSII layout.

    Args:
        path: Filesystem path to the GDS or GDSII file.

    Returns:
        Mapping from layer name to polygons read from ``path``.
    """
    return read_gds_layout(path).polygons_by_layer

read_oas_layout(path)

Read polygons and unit metadata from an OASIS layout.

Parameters:

Name Type Description Default
path Path

Filesystem path to the OASIS file.

required

Returns:

Type Description
LayoutData

Normalized layout payload containing polygons grouped by layer plus the

LayoutData

unit metadata reported by the OASIS library.

Source code in tempura/layout/readers.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def read_oas_layout(path: Path) -> LayoutData:
    """Read polygons and unit metadata from an OASIS layout.

    Args:
        path: Filesystem path to the OASIS file.

    Returns:
        Normalized layout payload containing polygons grouped by layer plus the
        unit metadata reported by the OASIS library.
    """
    library = gdstk.read_oas(path)
    return _layout_data(
        _group_cell_polygons(library.top_level()),
        unit_scale_m=float(library.unit),
        unit_source="oas_library.unit",
        precision_m=float(library.precision),
        units_known=True,
        source_format="oas",
    )

read_oas_polygons(path)

Read polygons from an OASIS layout.

Parameters:

Name Type Description Default
path Path

Filesystem path to the OASIS file.

required

Returns:

Type Description
PolygonMap

Mapping from layer name to polygons read from path.

Source code in tempura/layout/readers.py
318
319
320
321
322
323
324
325
326
327
def read_oas_polygons(path: Path) -> PolygonMap:
    """Read polygons from an OASIS layout.

    Args:
        path: Filesystem path to the OASIS file.

    Returns:
        Mapping from layer name to polygons read from ``path``.
    """
    return read_oas_layout(path).polygons_by_layer