Compare commits

..

No commits in common. "v0.9" and "master" have entirely different histories.

10 changed files with 1304 additions and 1945 deletions

2
.gitignore vendored
View file

@ -1,9 +1,7 @@
*.pyc *.pyc
__pycache__ __pycache__
*.idea *.idea
.mypy_cache/
build build
dist dist
fatamorgana.egg-info fatamorgana.egg-info
docs

View file

@ -1,3 +1,2 @@
include README.md include README.md
include LICENSE.md include LICENSE.md
include fatamorgana/VERSION

View file

@ -26,12 +26,12 @@
Install with pip from PyPi (preferred): Install with pip from PyPi (preferred):
```bash ```bash
pip3 install fatamorgana pip install fatamorgana
``` ```
Install directly from git repository: Install directly from git repository:
```bash ```bash
pip3 install git+https://mpxd.net/code/jan/fatamorgana.git@release pip install git+https://mpxd.net/code/jan/fatamorgana.git@release
``` ```
## Documentation ## Documentation

View file

@ -1 +0,0 @@
0.9

View file

@ -16,26 +16,13 @@
Dependencies: Dependencies:
- Python 3.5 or later - Python 3.5 or later
- numpy (optional, faster but no additional functionality) - numpy (optional, no additional functionality)
To get started, try:
```python3
import fatamorgana
help(fatamorgana.OasisLayout)
```
""" """
import pathlib
from .main import OasisLayout, Cell, XName from .main import OasisLayout, Cell, XName
from .basic import NString, AString, Validation, OffsetTable, OffsetEntry, \ from .basic import NString, AString, Validation, OffsetTable, OffsetEntry, \
EOFError, SignedError, InvalidDataError, InvalidRecordError, \ EOFError, SignedError, InvalidDataError, InvalidRecordError
UnfilledModalError, \
ReuseRepetition, GridRepetition, ArbitraryRepetition
__author__ = 'Jan Petykiewicz' __author__ = 'Jan Petykiewicz'
with open(pathlib.Path(__file__).parent / 'VERSION', 'r') as f: version = '0.4'
__version__ = f.read().strip()
version = __version__

File diff suppressed because it is too large Load diff

View file

@ -3,12 +3,11 @@ This module contains data structures and functions for reading from and
writing to whole OASIS layout files, and provides a few additional writing to whole OASIS layout files, and provides a few additional
abstractions for the data contained inside them. abstractions for the data contained inside them.
""" """
from typing import List, Dict, Union, Optional, Type
import io import io
import logging import logging
from . import records from . import records
from .records import Modals, Record from .records import Modals
from .basic import OffsetEntry, OffsetTable, NString, AString, real_t, Validation, \ from .basic import OffsetEntry, OffsetTable, NString, AString, real_t, Validation, \
read_magic_bytes, write_magic_bytes, read_uint, EOFError, \ read_magic_bytes, write_magic_bytes, read_uint, EOFError, \
InvalidDataError, InvalidRecordError InvalidDataError, InvalidRecordError
@ -16,7 +15,6 @@ from .basic import OffsetEntry, OffsetTable, NString, AString, real_t, Validatio
__author__ = 'Jan Petykiewicz' __author__ = 'Jan Petykiewicz'
#logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -25,21 +23,17 @@ class FileModals:
""" """
File-scoped modal variables File-scoped modal variables
""" """
cellname_implicit: Optional[bool] = None cellname_implicit = None # type: bool or None
propname_implicit: Optional[bool] = None propname_implicit = None # type: bool or None
xname_implicit: Optional[bool] = None xname_implicit = None # type: bool or None
textstring_implicit: Optional[bool] = None textstring_implicit = None # type: bool or None
propstring_implicit: Optional[bool] = None propstring_implicit = None # type: bool or None
cellname_implicit = None # type: bool or None
property_target: List[records.Property] within_cell = False # type: bool
within_cblock = False # type: bool
within_cell: bool = False end_has_offset_table = None # type: bool
within_cblock: bool = False started = False # type: bool
end_has_offset_table: bool = False
started: bool = False
def __init__(self, property_target = List[records.Property]):
self.property_target = property_target
class OasisLayout: class OasisLayout:
@ -49,50 +43,49 @@ class OasisLayout:
Names and strings are stored in dicts, indexed by reference number. Names and strings are stored in dicts, indexed by reference number.
Layer names and properties are stored directly using their associated Layer names and properties are stored directly using their associated
record objects. record objects.
Cells are stored using `Cell` objects (different from `records.Cell` Cells are stored using Cell objects (different from Cell record objects).
record objects).
Attributes: Properties:
(File properties) File properties:
version (AString): Version string ('1.0') .version AString: Version string ('1.0')
unit (real_t): grid steps per micron .unit real number: grid steps per micron
validation (Validation): checksum data .validation Validation: checksum data
(Names) Names:
cellnames (Dict[int, CellName]): Cell names .cellnames Dict[int, NString]
propnames (Dict[int, NString]): Property names .propnames Dict[int, NString]
xnames (Dict[int, XName]): Custom names .xnames Dict[int, XName]
(Strings) Strings:
textstrings (Dict[int, AString]): Text strings .textstrings Dict[int, AString]
propstrings (Dict[int, AString]): Property strings .propstrings Dict[int, AString]
(Data) Data:
layers (List[records.LayerName]): Layer definitions .layers List[records.LayerName]
properties (List[records.Property]): Property values .properties List[records.Property]
cells (List[Cell]): Layout cells .cells List[Cell]
""" """
version: AString version = None # type: AString
unit: real_t unit = None # type: real_t
validation: Validation validation = None # type: Validation
properties: List[records.Property] properties = None # type: List[records.Property]
cells: List['Cell'] cells = None # type: List[Cell]
cellnames: Dict[int, 'CellName'] cellnames = None # type: Dict[int, NString]
propnames: Dict[int, NString] propnames = None # type: Dict[int, NString]
xnames: Dict[int, 'XName'] xnames = None # type: Dict[int, XName]
textstrings = None # type: Dict[int, AString]
propstrings = None # type: Dict[int, AString]
layers = None # type: List[records.LayerName]
textstrings: Dict[int, AString]
propstrings: Dict[int, AString]
layers: List[records.LayerName]
def __init__(self, unit: real_t, validation: Validation = None): def __init__(self, unit: real_t, validation: Validation = None):
""" """
Args: :param unit: Real number (i.e. int, float, or Fraction), grid steps per micron.
unit: Real number (i.e. int, float, or `Fraction`), grid steps per micron. :param validation: Validation object containing checksum data.
validation: `Validation` object containing checksum data. Default creates a Validation object of the "no checksum" type.
Default creates a `Validation` object of the "no checksum" type.
""" """
if validation is None: if validation is None:
validation = Validation(0) validation = Validation(0)
@ -112,17 +105,14 @@ class OasisLayout:
@staticmethod @staticmethod
def read(stream: io.BufferedIOBase) -> 'OasisLayout': def read(stream: io.BufferedIOBase) -> 'OasisLayout':
""" """
Read an entire .oas file into an `OasisLayout` object. Read an entire .oas file into an OasisLayout object.
Args: :param stream: Stream to read from.
stream: Stream to read from. :return: New OasisLayout object.
Returns:
New `OasisLayout` object.
""" """
layout = OasisLayout(unit=-1) # dummy unit file_state = FileModals()
modals = Modals() modals = Modals()
file_state = FileModals(layout.properties) layout = OasisLayout(unit=None)
read_magic_bytes(stream) read_magic_bytes(stream)
@ -137,20 +127,15 @@ class OasisLayout:
) -> bool: ) -> bool:
""" """
Read a single record of unspecified type from a stream, adding its Read a single record of unspecified type from a stream, adding its
contents into this `OasisLayout` object. contents into this OasisLayout object.
Args: :param stream: Stream to read from.
stream: Stream to read from. :param modals: Modal variable data, used to fill unfilled record
modals: Modal variable data, used to fill unfilled record
fields and updated using filled record fields. fields and updated using filled record fields.
file_state: File status data. :param file_state: File status data.
:return: True if EOF was reached without error, False otherwise.
Returns: :raises: InvalidRecordError from unexpected records;
`True` if EOF was reached without error, `False` otherwise. InvalidDataError from within record parsers.
Raises:
InvalidRecordError: from unexpected records
InvalidDataError: from within record parsers
""" """
try: try:
record_id = read_uint(stream) record_id = read_uint(stream)
@ -162,8 +147,6 @@ class OasisLayout:
logger.info('read_record of type {} at position 0x{:x}'.format(record_id, stream.tell())) logger.info('read_record of type {} at position 0x{:x}'.format(record_id, stream.tell()))
record: Record
# CBlock # CBlock
if record_id == 34: if record_id == 34:
if file_state.within_cblock: if file_state.within_cblock:
@ -202,19 +185,16 @@ class OasisLayout:
raise InvalidRecordError('Unknown record id: {}'.format(record_id)) raise InvalidRecordError('Unknown record id: {}'.format(record_id))
if record_id == 0: if record_id == 0:
''' Pad ''' # Pad
pass pass
elif record_id == 1: elif record_id == 1:
''' Start '''
record = records.Start.read(stream, record_id) record = records.Start.read(stream, record_id)
record.merge_with_modals(modals) record.merge_with_modals(modals)
self.unit = record.unit self.unit = record.unit
self.version = record.version self.version = record.version
file_state.end_has_offset_table = record.offset_table is None file_state.end_has_offset_table = record.offset_table is None
file_state.property_target = self.properties
# TODO Offset table strict check # TODO Offset table strict check
elif record_id == 2: elif record_id == 2:
''' End '''
record = records.End.read(stream, record_id, file_state.end_has_offset_table) record = records.End.read(stream, record_id, file_state.end_has_offset_table)
record.merge_with_modals(modals) record.merge_with_modals(modals)
self.validation = record.validation self.validation = record.validation
@ -222,7 +202,6 @@ class OasisLayout:
raise InvalidRecordError('Stream continues past End record') raise InvalidRecordError('Stream continues past End record')
return True return True
elif record_id in (3, 4): elif record_id in (3, 4):
''' CellName '''
implicit = record_id == 3 implicit = record_id == 3
if file_state.cellname_implicit is None: if file_state.cellname_implicit is None:
file_state.cellname_implicit = implicit file_state.cellname_implicit = implicit
@ -234,12 +213,8 @@ class OasisLayout:
key = record.reference_number key = record.reference_number
if key is None: if key is None:
key = len(self.cellnames) key = len(self.cellnames)
self.cellnames[key] = record.nstring
cellname = CellName.from_record(record)
self.cellnames[key] = cellname
file_state.property_target = cellname.properties
elif record_id in (5, 6): elif record_id in (5, 6):
''' TextString '''
implicit = record_id == 5 implicit = record_id == 5
if file_state.textstring_implicit is None: if file_state.textstring_implicit is None:
file_state.textstring_implicit = implicit file_state.textstring_implicit = implicit
@ -253,7 +228,6 @@ class OasisLayout:
key = len(self.textstrings) key = len(self.textstrings)
self.textstrings[key] = record.astring self.textstrings[key] = record.astring
elif record_id in (7, 8): elif record_id in (7, 8):
''' PropName '''
implicit = record_id == 7 implicit = record_id == 7
if file_state.propname_implicit is None: if file_state.propname_implicit is None:
file_state.propname_implicit = implicit file_state.propname_implicit = implicit
@ -267,7 +241,6 @@ class OasisLayout:
key = len(self.propnames) key = len(self.propnames)
self.propnames[key] = record.nstring self.propnames[key] = record.nstring
elif record_id in (9, 10): elif record_id in (9, 10):
''' PropString '''
implicit = record_id == 9 implicit = record_id == 9
if file_state.propstring_implicit is None: if file_state.propstring_implicit is None:
file_state.propstring_implicit = implicit file_state.propstring_implicit = implicit
@ -281,17 +254,17 @@ class OasisLayout:
key = len(self.propstrings) key = len(self.propstrings)
self.propstrings[key] = record.astring self.propstrings[key] = record.astring
elif record_id in (11, 12): elif record_id in (11, 12):
''' LayerName '''
record = records.LayerName.read(stream, record_id) record = records.LayerName.read(stream, record_id)
record.merge_with_modals(modals) record.merge_with_modals(modals)
self.layers.append(record) self.layers.append(record)
elif record_id in (28, 29): elif record_id in (28, 29):
''' Property '''
record = records.Property.read(stream, record_id) record = records.Property.read(stream, record_id)
record.merge_with_modals(modals) record.merge_with_modals(modals)
file_state.property_target.append(record) if not file_state.within_cell:
self.properties.append(record)
else:
self.cells[-1].properties.append(record)
elif record_id in (30, 31): elif record_id in (30, 31):
''' XName '''
implicit = record_id == 30 implicit = record_id == 30
if file_state.xname_implicit is None: if file_state.xname_implicit is None:
file_state.xname_implicit = implicit file_state.xname_implicit = implicit
@ -304,34 +277,25 @@ class OasisLayout:
if key is None: if key is None:
key = len(self.xnames) key = len(self.xnames)
self.xnames[key] = XName.from_record(record) self.xnames[key] = XName.from_record(record)
# TODO: do anything with property target?
# #
# Cell and elements # Cell and elements
# #
elif record_id in (13, 14): elif record_id in (13, 14):
''' Cell '''
record = records.Cell.read(stream, record_id) record = records.Cell.read(stream, record_id)
record.merge_with_modals(modals) record.merge_with_modals(modals)
cell = Cell(record.name) self.cells.append(Cell(record.name))
self.cells.append(cell)
file_state.property_target = cell.properties
elif record_id in (15, 16): elif record_id in (15, 16):
''' XYMode '''
record = records.XYMode.read(stream, record_id) record = records.XYMode.read(stream, record_id)
record.merge_with_modals(modals) record.merge_with_modals(modals)
elif record_id in (17, 18): elif record_id in (17, 18):
''' Placement '''
record = records.Placement.read(stream, record_id) record = records.Placement.read(stream, record_id)
record.merge_with_modals(modals) record.merge_with_modals(modals)
self.cells[-1].placements.append(record) self.cells[-1].placements.append(record)
file_state.property_target = record.properties
elif record_id in _GEOMETRY: elif record_id in _GEOMETRY:
''' Geometry '''
record = _GEOMETRY[record_id].read(stream, record_id) record = _GEOMETRY[record_id].read(stream, record_id)
record.merge_with_modals(modals) record.merge_with_modals(modals)
self.cells[-1].geometry.append(record) self.cells[-1].geometry.append(record)
file_state.property_target = record.properties
else: else:
raise InvalidRecordError('Unknown record id: {}'.format(record_id)) raise InvalidRecordError('Unknown record id: {}'.format(record_id))
return False return False
@ -340,33 +304,26 @@ class OasisLayout:
""" """
Write this object in OASIS fromat to a stream. Write this object in OASIS fromat to a stream.
Args: :param stream: Stream to write to.
stream: Stream to write to. :return: Number of bytes written.
:raises: InvalidDataError if contained records are invalid.
Returns:
Number of bytes written.
Raises:
InvalidDataError: if contained records are invalid.
""" """
modals = Modals() modals = Modals()
size = 0 size = 0
size += write_magic_bytes(stream) size += write_magic_bytes(stream)
size += records.Start(self.unit, self.version).dedup_write(stream, modals) size += records.Start(self.unit, self.version).dedup_write(stream, modals)
size += sum(p.dedup_write(stream, modals) for p in self.properties)
cellnames_offset = OffsetEntry(False, size) cellnames_offset = OffsetEntry(False, size)
for refnum, cn in self.cellnames.items(): size += sum(records.CellName(name, refnum).dedup_write(stream, modals)
size += records.CellName(cn.nstring, refnum).dedup_write(stream, modals) for refnum, name in self.cellnames.items())
size += sum(p.dedup_write(stream, modals) for p in cn.properties)
propnames_offset = OffsetEntry(False, size) propnames_offset = OffsetEntry(False, size)
size += sum(records.PropName(name, refnum).dedup_write(stream, modals) size += sum(records.PropName(name, refnum).dedup_write(stream, modals)
for refnum, name in self.propnames.items()) for refnum, name in self.propnames.items())
xnames_offset = OffsetEntry(False, size) xnames_offset = OffsetEntry(False, size)
size += sum(records.XName(x.attribute, x.bstring, refnum).dedup_write(stream, modals) size += sum(records.XName(x.attribute, x.string, refnum).dedup_write(stream, modals)
for refnum, x in self.xnames.items()) for refnum, x in self.xnames.items())
textstrings_offset = OffsetEntry(False, size) textstrings_offset = OffsetEntry(False, size)
@ -380,6 +337,8 @@ class OasisLayout:
layernames_offset = OffsetEntry(False, size) layernames_offset = OffsetEntry(False, size)
size += sum(r.dedup_write(stream, modals) for r in self.layers) size += sum(r.dedup_write(stream, modals) for r in self.layers)
size += sum(p.dedup_write(stream, modals) for p in self.properties)
size += sum(c.dedup_write(stream, modals) for c in self.cells) size += sum(c.dedup_write(stream, modals) for c in self.cells)
offset_table = OffsetTable( offset_table = OffsetTable(
@ -398,110 +357,58 @@ class Cell:
""" """
Representation of an OASIS cell. Representation of an OASIS cell.
Attributes: Properties:
name (Union[NString, int]): name or "CellName reference" number .name NString or int (CellName reference number)
properties (List[records.Property]): Properties of this cell .properties List of records.Property
placements (List[records.Placement]): Placement record objects .placements List of records.Placement
geometry: (List[records.geometry_t]): Geometry record objectes .geometry List of geometry record objectes
""" """
name: Union[NString, int] name = None # type: NString or int
properties: List[records.Property] properties = None # type: List[records.Property]
placements: List[records.Placement] placements = None # type: List[records.Placement]
geometry: List[records.geometry_t] geometry = None # type: List[records.geometry_t]
def __init__(self, def __init__(self, name: NString or int):
name: Union[NString, str, int], """
*, :param name: NString or int (CellName reference number)
properties: Optional[List[records.Property]] = None, """
placements: Optional[List[records.Placement]] = None, self.name = name
geometry: Optional[List[records.geometry_t]] = None, self.properties = []
): self.placements = []
self.name = name if isinstance(name, (NString, int)) else NString(name) self.geometry = []
self.properties = [] if properties is None else properties
self.placements = [] if placements is None else placements
self.geometry = [] if geometry is None else geometry
def dedup_write(self, stream: io.BufferedIOBase, modals: Modals) -> int: def dedup_write(self, stream: io.BufferedIOBase, modals: Modals) -> int:
""" """
Write this cell to a stream, using the provided modal variables to Write this cell to a stream, using the provided modal variables to
deduplicate any repeated data. deduplicate any repeated data.
Args: :param stream: Stream to write to.
stream: Stream to write to. :param modals: Modal variables to use for deduplication.
modals: Modal variables to use for deduplication. :return: Number of bytes written.
:raises: InvalidDataError if contained records are invalid.
Returns:
Number of bytes written.
Raises:
InvalidDataError: if contained records are invalid.
""" """
size = records.Cell(self.name).dedup_write(stream, modals) size = records.Cell(self.name).dedup_write(stream, modals)
size += sum(p.dedup_write(stream, modals) for p in self.properties) size += sum(p.dedup_write(stream, modals) for p in self.properties)
for placement in self.placements: size += sum(p.dedup_write(stream, modals) for p in self.placements)
size += placement.dedup_write(stream, modals) size += sum(g.dedup_write(stream, modals) for g in self.geometry)
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)
return size 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
properties: List[records.Property]
def __init__(self,
nstring: Union[NString, str],
properties: Optional[List[records.Property]] = 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)
class XName: class XName:
""" """
Representation of an XName. Representation of an XName.
This class is effectively a simplified form of a `records.XName`, This class is effectively a simplified form of a records.XName,
with the reference data stripped out. with the reference data stripped out.
""" """
attribute: int attribute = None # type: int
bstring: bytes bstring = None # type: bytes
def __init__(self, attribute: int, bstring: bytes): def __init__(self, attribute: int, bstring: bytes):
""" """
Args: :param attribute: Attribute number.
attribute: Attribute number. :param bstring: Binary data.
bstring: Binary data.
""" """
self.attribute = attribute self.attribute = attribute
self.bstring = bstring self.bstring = bstring
@ -509,19 +416,16 @@ class XName:
@staticmethod @staticmethod
def from_record(record: records.XName) -> 'XName': def from_record(record: records.XName) -> 'XName':
""" """
Create an `XName` object from a `records.XName` record. Create an XName object from a records.XName record.
Args: :param record: XName record to use.
record: XName record to use. :return: XName object.
Returns:
a new `XName` object.
""" """
return XName(record.attribute, record.bstring) return XName(record.attribute, record.bstring)
# Mapping from record id to record class. # Mapping from record id to record class.
_GEOMETRY: Dict[int, Type[records.geometry_t]] = { _GEOMETRY = {
19: records.Text, 19: records.Text,
20: records.Rectangle, 20: records.Rectangle,
21: records.Polygon, 21: records.Polygon,

View file

File diff suppressed because it is too large Load diff

View file

@ -1,45 +1,19 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from setuptools import setup, find_packages from setuptools import setup, find_packages
import fatamorgana
with open('README.md', 'r') as f: with open('README.md', 'r') as f:
long_description = f.read() long_description = f.read()
with open('fatamorgana/VERSION', 'r') as f:
version = f.read().strip()
setup(name='fatamorgana', setup(name='fatamorgana',
version=version, version=fatamorgana.version,
description='OASIS layout format parser and writer', description='OASIS layout format parser and writer',
long_description=long_description, long_description=long_description,
long_description_content_type='text/markdown', long_description_content_type='text/markdown',
author='Jan Petykiewicz', author='Jan Petykiewicz',
author_email='anewusername@gmail.com', author_email='anewusername@gmail.com',
url='https://mpxd.net/code/jan/fatamorgana', url='https://mpxd.net/code/jan/fatamorgana',
packages=find_packages(),
package_data={
'fatamorgana': ['VERSION',
'py.typed',
],
},
install_requires=[
'typing',
],
extras_require={
'numpy': ['numpy'],
},
classifiers=[
'Programming Language :: Python :: 3',
'Development Status :: 3 - Alpha',
'Environment :: Other Environment',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'Intended Audience :: Manufacturing',
'Intended Audience :: Science/Research',
'License :: OSI Approved :: GNU Affero General Public License v3',
'Topic :: Scientific/Engineering',
'Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)',
],
keywords=[ keywords=[
'OASIS', 'OASIS',
'layout', 'layout',
@ -60,5 +34,28 @@ setup(name='fatamorgana',
'polygon', 'polygon',
'gds', 'gds',
], ],
classifiers=[
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Development Status :: 3 - Alpha',
'Environment :: Other Environment',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'Intended Audience :: Manufacturing',
'Intended Audience :: Science/Research',
'License :: OSI Approved :: GNU Affero General Public License v3',
'Operating System :: POSIX :: Linux',
'Operating System :: Microsoft :: Windows',
'Topic :: Scientific/Engineering',
'Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)',
'Topic :: Software Development :: Libraries :: Python Modules',
],
packages=find_packages(),
install_requires=[
'typing',
],
extras_require={
'numpy': ['numpy'],
},
) )