543 lines
18 KiB
Python
Raw Normal View History

2017-09-18 03:01:48 -07:00
"""
This module contains data structures and functions for reading from and
writing to whole OASIS layout files, and provides a few additional
abstractions for the data contained inside them.
"""
2024-07-29 03:53:08 -07:00
from typing import IO
2017-09-18 03:01:48 -07:00
import io
2018-07-21 13:39:47 -07:00
import logging
2017-09-18 03:01:48 -07:00
from . import records
2020-04-18 15:38:52 -07:00
from .records import Modals, Record
2020-10-16 19:00:00 -07:00
from .basic import (
OffsetEntry, OffsetTable, NString, AString, real_t, Validation,
read_magic_bytes, write_magic_bytes, read_uint, EOFError,
InvalidRecordError,
)
2017-09-18 03:01:48 -07:00
__author__ = 'Jan Petykiewicz'
2020-09-10 20:03:19 -07:00
#logging.basicConfig(level=logging.DEBUG)
2017-09-18 03:01:48 -07:00
2018-07-21 13:39:47 -07:00
logger = logging.getLogger(__name__)
2017-09-18 03:01:48 -07:00
class FileModals:
"""
File-scoped modal variables
"""
2024-03-30 19:44:47 -07:00
cellname_implicit: bool | None = None
propname_implicit: bool | None = None
xname_implicit: bool | None = None
textstring_implicit: bool | None = None
propstring_implicit: bool | None = None
2017-09-18 03:01:48 -07:00
2024-03-30 19:44:47 -07:00
property_target: list[records.Property]
within_cell: bool = False
within_cblock: bool = False
end_has_offset_table: bool = False
started: bool = False
2017-09-18 03:01:48 -07:00
2024-03-30 19:44:47 -07:00
def __init__(self, property_target: list[records.Property]) -> None:
self.property_target = property_target
2017-09-18 03:01:48 -07:00
class OasisLayout:
"""
Representation of a full OASIS layout file.
Names and strings are stored in dicts, indexed by reference number.
Layer names and properties are stored directly using their associated
record objects.
Cells are stored using `Cell` objects (different from `records.Cell`
record objects).
2017-09-18 03:01:48 -07:00
"""
2024-03-30 19:44:47 -07:00
# File properties
version: AString
2024-03-30 19:44:47 -07:00
"""File format version string ('1.0')"""
unit: real_t
2024-03-30 19:44:47 -07:00
"""grid steps per micron"""
validation: Validation
2024-03-30 19:44:47 -07:00
"""checksum data"""
# Data
properties: list[records.Property]
"""Property values"""
cells: list['Cell']
"""Layout cells"""
layers: list[records.LayerName]
"""Layer definitions"""
2017-09-18 03:01:48 -07:00
2024-03-30 19:44:47 -07:00
# Names
cellnames: dict[int, 'CellName']
"""Cell names"""
2017-09-18 03:01:48 -07:00
2024-03-30 19:44:47 -07:00
propnames: dict[int, NString]
"""Property names"""
2017-09-18 03:01:48 -07:00
2024-03-30 19:44:47 -07:00
xnames: dict[int, 'XName']
"""Custom names"""
# String storage
textstrings: dict[int, AString]
"""Text strings"""
propstrings: dict[int, AString]
"""Property strings"""
2017-09-18 03:01:48 -07:00
def __init__(
self,
unit: real_t,
2024-03-30 19:44:47 -07:00
validation: Validation | None = None,
) -> None:
2017-09-18 03:01:48 -07:00
"""
Args:
unit: Real number (i.e. int, float, or `Fraction`), grid steps per micron.
validation: `Validation` object containing checksum data.
Default creates a `Validation` object of the "no checksum" type.
2017-09-18 03:01:48 -07:00
"""
if validation is None:
validation = Validation(0)
self.unit = unit
self.validation = validation
self.version = AString("1.0")
self.properties = []
self.cells = []
self.cellnames = {}
self.propnames = {}
self.xnames = {}
self.textstrings = {}
self.propstrings = {}
self.layers = []
@staticmethod
def read(stream: IO[bytes]) -> 'OasisLayout':
2017-09-18 03:01:48 -07:00
"""
Read an entire .oas file into an `OasisLayout` object.
2017-09-18 03:01:48 -07:00
Args:
stream: Stream to read from.
Returns:
New `OasisLayout` object.
2017-09-18 03:01:48 -07:00
"""
layout = OasisLayout(unit=-1) # dummy unit
modals = Modals()
file_state = FileModals(layout.properties)
2017-09-18 03:01:48 -07:00
read_magic_bytes(stream)
while not layout.read_record(stream, modals, file_state):
pass
return layout
def read_record(
self,
stream: IO[bytes],
modals: Modals,
file_state: FileModals
) -> bool:
2017-09-18 03:01:48 -07:00
"""
Read a single record of unspecified type from a stream, adding its
contents into this `OasisLayout` object.
Args:
stream: Stream to read from.
modals: Modal variable data, used to fill unfilled record
fields and updated using filled record fields.
file_state: File status data.
Returns:
`True` if EOF was reached without error, `False` otherwise.
Raises:
InvalidRecordError: from unexpected records
InvalidDataError: from within record parsers
2017-09-18 03:01:48 -07:00
"""
try:
record_id = read_uint(stream)
except EOFError as e:
if file_state.within_cblock:
return True
else:
raise e
logger.info(f'read_record of type {record_id} at position 0x{stream.tell():x}')
2017-09-18 03:01:48 -07:00
2020-04-18 15:38:52 -07:00
record: Record
2017-09-18 03:01:48 -07:00
# CBlock
if record_id == 34:
if file_state.within_cblock:
raise InvalidRecordError('Nested CBlock')
record = records.CBlock.read(stream, record_id)
decoded_data = record.decompress()
file_state.within_cblock = True
decoded_stream = io.BytesIO(decoded_data)
while not self.read_record(decoded_stream, modals, file_state):
pass
file_state.within_cblock = False
return False
# Make sure order is valid (eg, no out-of-cell geometry)
if not file_state.started and record_id != 1:
raise InvalidRecordError(f'Non-Start record {record_id} before Start')
2017-09-18 03:01:48 -07:00
if record_id == 1:
if file_state.started:
raise InvalidRecordError('Duplicate Start record')
2024-07-29 18:06:59 -07:00
file_state.started = True
2017-09-18 03:01:48 -07:00
if record_id == 2 and file_state.within_cblock:
raise InvalidRecordError('End within CBlock')
if record_id in (0, 1, 2, 28, 29):
pass
elif record_id in range(3, 13) or record_id in (28, 29):
file_state.within_cell = False
elif record_id in range(15, 28) or record_id in (32, 33):
2017-09-18 03:01:48 -07:00
if not file_state.within_cell:
raise Exception('Geometry outside Cell')
2018-07-21 13:41:19 -07:00
elif record_id in (13, 14):
2017-09-18 03:01:48 -07:00
file_state.within_cell = True
else:
raise InvalidRecordError(f'Unknown record id: {record_id}')
2017-09-18 03:01:48 -07:00
if record_id == 0:
2020-09-10 20:03:19 -07:00
''' Pad '''
2017-09-18 03:01:48 -07:00
pass
elif record_id == 1:
2020-09-10 20:03:19 -07:00
''' Start '''
2017-09-18 03:01:48 -07:00
record = records.Start.read(stream, record_id)
record.merge_with_modals(modals)
self.unit = record.unit
self.version = record.version
file_state.end_has_offset_table = record.offset_table is None
2020-09-10 20:03:19 -07:00
file_state.property_target = self.properties
2017-09-18 03:01:48 -07:00
# TODO Offset table strict check
elif record_id == 2:
2020-09-10 20:03:19 -07:00
''' End '''
2017-09-18 03:01:48 -07:00
record = records.End.read(stream, record_id, file_state.end_has_offset_table)
record.merge_with_modals(modals)
self.validation = record.validation
if not len(stream.read(1)) == 0:
raise InvalidRecordError('Stream continues past End record')
return True
elif record_id in (3, 4):
2020-09-10 20:03:19 -07:00
''' CellName '''
2017-09-18 03:01:48 -07:00
implicit = record_id == 3
if file_state.cellname_implicit is None:
file_state.cellname_implicit = implicit
elif file_state.cellname_implicit != implicit:
raise InvalidRecordError('Mix of implicit and non-implicit cellnames')
record = records.CellName.read(stream, record_id)
record.merge_with_modals(modals)
key = record.reference_number
if key is None:
key = len(self.cellnames)
cellname = CellName.from_record(record)
self.cellnames[key] = cellname
file_state.property_target = cellname.properties
2017-09-18 03:01:48 -07:00
elif record_id in (5, 6):
2020-09-10 20:03:19 -07:00
''' TextString '''
2017-09-18 03:01:48 -07:00
implicit = record_id == 5
if file_state.textstring_implicit is None:
file_state.textstring_implicit = implicit
elif file_state.textstring_implicit != implicit:
raise InvalidRecordError('Mix of implicit and non-implicit textstrings')
record = records.TextString.read(stream, record_id)
record.merge_with_modals(modals)
key = record.reference_number
if key is None:
key = len(self.textstrings)
self.textstrings[key] = record.astring
elif record_id in (7, 8):
2020-09-10 20:03:19 -07:00
''' PropName '''
2017-09-18 03:01:48 -07:00
implicit = record_id == 7
if file_state.propname_implicit is None:
file_state.propname_implicit = implicit
elif file_state.propname_implicit != implicit:
raise InvalidRecordError('Mix of implicit and non-implicit propnames')
record = records.PropName.read(stream, record_id)
record.merge_with_modals(modals)
key = record.reference_number
if key is None:
key = len(self.propnames)
self.propnames[key] = record.nstring
elif record_id in (9, 10):
2020-09-10 20:03:19 -07:00
''' PropString '''
2017-09-18 03:01:48 -07:00
implicit = record_id == 9
if file_state.propstring_implicit is None:
file_state.propstring_implicit = implicit
elif file_state.propstring_implicit != implicit:
raise InvalidRecordError('Mix of implicit and non-implicit propstrings')
record = records.PropString.read(stream, record_id)
record.merge_with_modals(modals)
key = record.reference_number
if key is None:
key = len(self.propstrings)
self.propstrings[key] = record.astring
elif record_id in (11, 12):
2020-09-10 20:03:19 -07:00
''' LayerName '''
2017-09-18 03:01:48 -07:00
record = records.LayerName.read(stream, record_id)
record.merge_with_modals(modals)
self.layers.append(record)
elif record_id in (28, 29):
2020-09-10 20:03:19 -07:00
''' Property '''
2017-09-18 03:01:48 -07:00
record = records.Property.read(stream, record_id)
record.merge_with_modals(modals)
file_state.property_target.append(record)
2017-09-18 03:01:48 -07:00
elif record_id in (30, 31):
2020-09-10 20:03:19 -07:00
''' XName '''
2017-09-18 03:01:48 -07:00
implicit = record_id == 30
if file_state.xname_implicit is None:
file_state.xname_implicit = implicit
elif file_state.xname_implicit != implicit:
raise InvalidRecordError('Mix of implicit and non-implicit xnames')
record = records.XName.read(stream, record_id)
record.merge_with_modals(modals)
key = record.reference_number
if key is None:
key = len(self.xnames)
self.xnames[key] = XName.from_record(record)
# TODO: do anything with property target?
2017-09-18 03:01:48 -07:00
#
# Cell and elements
#
elif record_id in (13, 14):
2020-09-10 20:03:19 -07:00
''' Cell '''
2017-09-18 03:01:48 -07:00
record = records.Cell.read(stream, record_id)
record.merge_with_modals(modals)
cell = Cell(record.name)
self.cells.append(cell)
file_state.property_target = cell.properties
2017-09-18 03:01:48 -07:00
elif record_id in (15, 16):
2020-09-10 20:03:19 -07:00
''' XYMode '''
2017-09-18 03:01:48 -07:00
record = records.XYMode.read(stream, record_id)
record.merge_with_modals(modals)
elif record_id in (17, 18):
2020-09-10 20:03:19 -07:00
''' Placement '''
2017-09-18 03:01:48 -07:00
record = records.Placement.read(stream, record_id)
record.merge_with_modals(modals)
self.cells[-1].placements.append(record)
file_state.property_target = record.properties
2017-09-18 03:01:48 -07:00
elif record_id in _GEOMETRY:
2020-09-10 20:03:19 -07:00
''' Geometry '''
2017-09-18 03:01:48 -07:00
record = _GEOMETRY[record_id].read(stream, record_id)
record.merge_with_modals(modals)
self.cells[-1].geometry.append(record)
file_state.property_target = record.properties
2017-09-18 03:01:48 -07:00
else:
raise InvalidRecordError(f'Unknown record id: {record_id}')
2017-09-18 03:01:48 -07:00
return False
def write(self, stream: IO[bytes]) -> int:
2017-09-18 03:01:48 -07:00
"""
Write this object in OASIS fromat to a stream.
Args:
stream: Stream to write to.
Returns:
Number of bytes written.
Raises:
InvalidDataError: if contained records are invalid.
2017-09-18 03:01:48 -07:00
"""
modals = Modals()
size = 0
size += write_magic_bytes(stream)
size += records.Start(self.unit, self.version).dedup_write(stream, modals)
size += sum(p.dedup_write(stream, modals) for p in self.properties)
2017-09-18 03:01:48 -07:00
cellnames_offset = OffsetEntry(False, size)
for refnum, cn in self.cellnames.items():
size += records.CellName(cn.nstring, refnum).dedup_write(stream, modals)
size += sum(p.dedup_write(stream, modals) for p in cn.properties)
2017-09-18 03:01:48 -07:00
propnames_offset = OffsetEntry(False, size)
size += sum(records.PropName(name, refnum).dedup_write(stream, modals)
for refnum, name in self.propnames.items())
xnames_offset = OffsetEntry(False, size)
size += sum(records.XName(x.attribute, x.bstring, refnum).dedup_write(stream, modals)
2017-09-18 03:01:48 -07:00
for refnum, x in self.xnames.items())
textstrings_offset = OffsetEntry(False, size)
size += sum(records.TextString(s, refnum).dedup_write(stream, modals)
for refnum, s in self.textstrings.items())
propstrings_offset = OffsetEntry(False, size)
size += sum(records.PropString(s, refnum).dedup_write(stream, modals)
for refnum, s in self.propstrings.items())
layernames_offset = OffsetEntry(False, size)
size += sum(r.dedup_write(stream, modals) for r in self.layers)
size += sum(c.dedup_write(stream, modals) for c in self.cells)
offset_table = OffsetTable(
cellnames_offset,
textstrings_offset,
propnames_offset,
propstrings_offset,
layernames_offset,
xnames_offset,
)
size += records.End(self.validation, offset_table).dedup_write(stream, modals)
return size
class Cell:
"""
Representation of an OASIS cell.
"""
2024-03-30 19:44:47 -07:00
name: NString | int
"""name or "CellName reference" number"""
properties: list[records.Property]
placements: list[records.Placement]
geometry: list[records.geometry_t]
2017-09-18 03:01:48 -07:00
def __init__(
self,
2024-03-30 19:44:47 -07:00
name: NString | str | int,
*,
2024-03-30 19:44:47 -07:00
properties: list[records.Property] | None = None,
placements: list[records.Placement] | None = None,
geometry: list[records.geometry_t] | None = None,
) -> None:
self.name = name if isinstance(name, (NString, int)) else NString(name)
self.properties = [] if properties is None else properties
self.placements = [] if placements is None else placements
self.geometry = [] if geometry is None else geometry
2017-09-18 03:01:48 -07:00
def dedup_write(self, stream: IO[bytes], modals: Modals) -> int:
2017-09-18 03:01:48 -07:00
"""
Write this cell to a stream, using the provided modal variables to
deduplicate any repeated data.
Args:
stream: Stream to write to.
modals: Modal variables to use for deduplication.
Returns:
Number of bytes written.
Raises:
InvalidDataError: if contained records are invalid.
2017-09-18 03:01:48 -07:00
"""
size = records.Cell(self.name).dedup_write(stream, modals)
size += sum(p.dedup_write(stream, modals) for p in self.properties)
for placement in self.placements:
size += placement.dedup_write(stream, modals)
size += sum(p.dedup_write(stream, modals) for p in placement.properties)
for shape in self.geometry:
size += shape.dedup_write(stream, modals)
size += sum(p.dedup_write(stream, modals) for p in shape.properties)
2017-09-18 03:01:48 -07:00
return size
class CellName:
"""
Representation of a CellName.
This class is effectively a simplified form of a `records.CellName`,
with the reference data stripped out.
"""
nstring: NString
2024-03-30 19:44:47 -07:00
properties: list[records.Property]
def __init__(
self,
2024-03-30 19:44:47 -07:00
nstring: NString | str,
properties: list[records.Property] | None = None,
) -> None:
"""
Args:
nstring: The contained string.
properties: Properties which apply to this CellName's cell, but
are placed following the CellName record.
"""
if isinstance(nstring, NString):
self.nstring = nstring
else:
self.nstring = NString(nstring)
self.properties = [] if properties is None else properties
@staticmethod
def from_record(record: records.CellName) -> 'CellName':
"""
Create an `CellName` object from a `records.CellName` record.
Args:
record: CellName record to use.
Returns:
A new `CellName` object.
"""
return CellName(record.nstring)
2017-09-18 03:01:48 -07:00
class XName:
"""
Representation of an XName.
This class is effectively a simplified form of a `records.XName`,
2017-09-18 03:01:48 -07:00
with the reference data stripped out.
"""
attribute: int
bstring: bytes
2017-09-18 03:01:48 -07:00
def __init__(self, attribute: int, bstring: bytes) -> None:
2017-09-18 03:01:48 -07:00
"""
Args:
attribute: Attribute number.
bstring: Binary data.
2017-09-18 03:01:48 -07:00
"""
self.attribute = attribute
self.bstring = bstring
@staticmethod
def from_record(record: records.XName) -> 'XName':
"""
Create an `XName` object from a `records.XName` record.
Args:
record: XName record to use.
2017-09-18 03:01:48 -07:00
Returns:
a new `XName` object.
2017-09-18 03:01:48 -07:00
"""
return XName(record.attribute, record.bstring)
# Mapping from record id to record class.
2024-07-29 03:53:08 -07:00
_GEOMETRY: dict[int, type[records.geometry_t]] = {
2017-09-18 03:01:48 -07:00
19: records.Text,
20: records.Rectangle,
21: records.Polygon,
22: records.Path,
23: records.Trapezoid,
24: records.Trapezoid,
25: records.Trapezoid,
26: records.CTrapezoid,
27: records.Circle,
32: records.XElement,
33: records.XGeometry,
}