Compare commits

..

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

11 changed files with 1327 additions and 2006 deletions

29
.flake8
View file

@ -1,29 +0,0 @@
[flake8]
ignore =
# E501 line too long
E501,
# W391 newlines at EOF
W391,
# E241 multiple spaces after comma
E241,
# E302 expected 2 newlines
E302,
# W503 line break before binary operator (to be deprecated)
W503,
# E265 block comment should start with '# '
E265,
# E123 closing bracket does not match indentation of opening bracket's line
E123,
# E124 closing bracket does not match visual indentation
E124,
# E221 multiple spaces before operator
E221,
# E201 whitespace after '['
E201,
# E741 ambiguous variable name 'I'
E741,
per-file-ignores =
# F401 import without use
*/__init__.py: F401,

2
.gitignore vendored
View file

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

View file

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

View file

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

View file

@ -1,4 +0,0 @@
""" VERSION defintion. THIS FILE IS MANUALLY PARSED BY setup.py and REQUIRES A SPECIFIC FORMAT """
__version__ = '''
0.11
'''

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

File diff suppressed because it is too large Load diff

View file

@ -1,44 +1,19 @@
#!/usr/bin/env python3
from setuptools import setup, find_packages
import fatamorgana
with open('README.md', 'r') as f:
long_description = f.read()
with open('fatamorgana/VERSION.py', 'rt') as f:
version = f.readlines()[2].strip()
setup(name='fatamorgana',
version=version,
version=fatamorgana.version,
description='OASIS layout format parser and writer',
long_description=long_description,
long_description_content_type='text/markdown',
author='Jan Petykiewicz',
author_email='anewusername@gmail.com',
url='https://mpxd.net/code/jan/fatamorgana',
packages=find_packages(),
package_data={
'fatamorgana': ['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=[
'OASIS',
'layout',
@ -59,5 +34,28 @@ setup(name='fatamorgana',
'polygon',
'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'],
},
)